diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cf6901c54..5fee45904 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -17,19 +17,14 @@ grep -r "pattern" src/ rtk grep -r "pattern" src/ **rtk meta-commands** (always use these directly, no prefix needed): ```bash -rtk gain # Show token savings analytics -rtk gain --history # Full command history with per-command savings -rtk discover # Scan session history for missed rtk opportunities rtk proxy # Run a command raw (no filtering) but still track it ``` **Verify rtk is installed before starting:** ```bash rtk --version # Should print: rtk X.Y.Z -rtk gain # Should show a dashboard (not "command not found") ``` -> Name collision: `rtk gain` failing means you have `reachingforthejack/rtk` (Rust Type Kit) installed instead. Run `which rtk` to check. ## Build, Test & Lint diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 16ecfa716..abe212637 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,19 +1,13 @@ version: 2 updates: - - package-ecosystem: "cargo" - target-branch: "develop" + - package-ecosystem: cargo directory: "/" schedule: - interval: "weekly" - labels: - - "dependencies" - open-pull-requests-limit: 5 + interval: weekly + open-pull-requests-limit: 10 - - package-ecosystem: "github-actions" - target-branch: "develop" + - package-ecosystem: github-actions directory: "/" schedule: - interval: "weekly" - labels: - - "dependencies" - - "area:ci" + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/docs-pipeline-contract.md b/.github/docs-pipeline-contract.md deleted file mode 100644 index f812912cd..000000000 --- a/.github/docs-pipeline-contract.md +++ /dev/null @@ -1,57 +0,0 @@ -# RTK Documentation — Interface Contract - -This directory contains user-facing documentation for the RTK website. -It feeds `rtk-ai/rtk-website` via the `prepare-docs.mjs` pipeline. - -**Scope**: `docs/guide/` is website content only. Technical and contributor documentation -lives in the codebase (distributed, co-located pattern): -- `ARCHITECTURE.md` — System design, ADRs, filtering strategies -- `CONTRIBUTING.md` — Design philosophy, PR process, TOML vs Rust -- `SECURITY.md` — Vulnerability policy -- `src/*/README.md` — Per-module implementation docs -- `hooks/README.md` — Hook system and agent integrations - -## Structure - -``` -docs/ - README.md <- This file (interface contract — do not remove) - guide/ -> User-facing documentation (website "Guide" tab) - index.md - getting-started/ - installation.md - quick-start.md - supported-agents.md - what-rtk-covers.md - analytics/ - gain.md - configuration.md - troubleshooting.md -``` - -## Frontmatter (required on every .md) - -Every markdown file under `docs/guide/` must include: - -```yaml ---- -title: string # Page title (used in sidebar + search) -description: string # One-line summary for search results and SEO -sidebar: - order: number # Position within the sidebar group (1 = first) ---- -``` - -The `prepare-docs.mjs` pipeline validates this at build time and fails fast -if frontmatter is missing or malformed. - -## Conventions - -- **Filenames**: kebab-case, `.md` only -- **Subdirectories**: become sidebar groups in Starlight -- **Internal links**: relative (`./foo.md`, `../configuration.md`) -- **Diagrams**: Mermaid in fenced code blocks -- **Code samples**: always specify the language (`rust`, `toml`, `bash`) -- **Language**: English only -- **No `rtk ` syntax**: users never type `rtk` — hooks rewrite commands transparently. - Only `rtk gain`, `rtk init`, `rtk verify`, and `rtk proxy` appear as user-typed commands. diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..d4e8a43d1 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,17 @@ +name: Dependency Review + +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 000000000..10fc7e538 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,21 @@ +name: Dependency Submission + +on: + push: + branches: [master] + schedule: + - cron: "0 8 * * 1" + workflow_dispatch: + +permissions: + contents: write + +jobs: + submit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate and submit dependency snapshot + uses: advanced-security/cargo-lock-submission@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06cb022df..5a8294812 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,9 +81,6 @@ jobs: - name: Build run: cargo build --release --target ${{ matrix.target }} - env: - RTK_TELEMETRY_URL: ${{ vars.RTK_TELEMETRY_URL }} - RTK_TELEMETRY_TOKEN: ${{ secrets.RTK_TELEMETRY_TOKEN }} - name: Package (Unix) if: matrix.os != 'windows-latest' @@ -120,9 +117,6 @@ jobs: - name: Build DEB run: cargo deb - env: - RTK_TELEMETRY_URL: ${{ vars.RTK_TELEMETRY_URL }} - RTK_TELEMETRY_TOKEN: ${{ secrets.RTK_TELEMETRY_TOKEN }} - name: Upload DEB uses: actions/upload-artifact@v4 @@ -147,9 +141,6 @@ jobs: - name: Build release run: cargo build --release - env: - RTK_TELEMETRY_URL: ${{ vars.RTK_TELEMETRY_URL }} - RTK_TELEMETRY_TOKEN: ${{ secrets.RTK_TELEMETRY_TOKEN }} - name: Generate RPM run: cargo generate-rpm @@ -165,13 +156,6 @@ jobs: needs: [build, build-deb, build-rpm] runs-on: ubuntu-latest steps: - - uses: actions/create-github-app-token@v3 - id: app-token - with: - client-id: ${{ secrets.APP_CLIENT_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - permission-contents: write - - name: Checkout uses: actions/checkout@v4 @@ -215,147 +199,6 @@ jobs: tag_name: ${{ steps.version.outputs.version }} files: release/* prerelease: ${{ inputs.prerelease }} - token: ${{ steps.app-token.outputs.token }} - - notify-discord: - name: Notify Discord - needs: [release] - if: ${{ !inputs.prerelease }} - 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 - echo "tag=$TAG" >> $GITHUB_OUTPUT - - - name: Send Discord notification env: - DISCORD_WEBHOOK: ${{ secrets.RTK_DISCORD_RELEASE }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - TAG="${{ steps.version.outputs.tag }}" - RELEASE_URL="https://github.com/rtk-ai/rtk/releases/tag/${TAG}" - - # Fetch release notes from GitHub API - NOTES=$(gh api "repos/rtk-ai/rtk/releases/tags/${TAG}" --jq '.body' 2>/dev/null | head -c 1800 || echo "") - DESC=$(echo "${NOTES:-No release notes}" | jq -Rs .) + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jq -n \ - --arg title "RTK ${TAG} released" \ - --arg url "$RELEASE_URL" \ - --argjson desc "$DESC" \ - '{embeds: [{title: $title, url: $url, description: $desc, color: 5814783, footer: {text: "Rust Token Killer"}}]}' \ - | curl -sf -H "Content-Type: application/json" -d @- "$DISCORD_WEBHOOK" - - homebrew: - name: Update Homebrew formula - needs: [release] - if: ${{ !inputs.prerelease }} - 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 rtk-ai/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-musl.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/rtk-ai/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" - 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" - 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-musl.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/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..7ec60ac33 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,88 @@ +minimum_prek_version: "0.3.8" +fail_fast: true +default_install_hook_types: [pre-commit, pre-push] +default_stages: [pre-commit] + +repos: + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-merge-conflict + - id: check-case-conflict + - id: check-symlinks + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + - id: check-yaml + - id: check-toml + - id: check-added-large-files + args: ["--maxkb=256"] + + - repo: local + hooks: + - id: architecture-size-guard + name: architecture-size-guard + entry: bash scripts/prek/check-architecture-size.sh + language: system + pass_filenames: false + always_run: true + + - id: dangerous-patterns + name: dangerous-patterns-in-added-rust-lines + entry: bash scripts/prek/check-dangerous-patterns.sh + language: system + files: "\\.rs$" + + - id: cmd-module-has-tests + name: cmd-module-has-tests + entry: bash scripts/prek/check-cmd-tests.sh + language: system + files: "^src/cmds/.+_cmd\\.rs$" + + - id: new-command-wiring + name: new-command-wiring-guard + entry: bash scripts/prek/check-new-command-wiring.sh + language: system + pass_filenames: false + always_run: true + + - id: rust-fmt + name: rustfmt-all-check + entry: cargo fmt --all -- --check + language: system + pass_filenames: false + always_run: true + + - id: rust-check + name: cargo-check-all-targets + entry: cargo check --all-targets + language: system + pass_filenames: false + always_run: true + + - id: rust-clippy + name: cargo-clippy-all-targets + entry: cargo clippy --all-targets + language: system + pass_filenames: false + always_run: true + + - id: rust-tests + name: cargo-test-all + entry: cargo test --all + language: system + pass_filenames: false + always_run: true + + - id: cargo-audit + name: cargo-audit-security + entry: bash scripts/prek/run-cargo-audit.sh + language: system + pass_filenames: false + always_run: true + stages: [pre-push] diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e1beb46..88dedb97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1077 +1,5 @@ # Changelog -All notable changes to rtk (Rust Token Killer) will be documented in this file. +Historical changelog from upstream was removed in this fork. -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.39.0](https://github.com/rtk-ai/rtk/compare/v0.38.0...v0.39.0) (2026-05-06) - - -### Features - -* **cicd:** add auto next release parser ([bf24972](https://github.com/rtk-ai/rtk/commit/bf24972e7d463f0432b8315e3035e9eb13ff062f)) -* **cicd:** target develop branch ([63da7da](https://github.com/rtk-ai/rtk/commit/63da7dafd61b5f65115989aeda01f666a64457ff)) - - -### Bug Fixes - -* **cicd:** match ":" for body prefix to catch ([5987333](https://github.com/rtk-ai/rtk/commit/5987333209cd59c1e22f9e0b247ab390cb431dbf)) -* **cicd:** match allowed repo list in pr bodies ([b1233ab](https://github.com/rtk-ai/rtk/commit/b1233ab3fbc0927145d5c0f763725b098fc7dd99)) -* **curl:** gate force_tee_hint, extend JSON heuristic, avoid full-body alloc ([2ed53c7](https://github.com/rtk-ai/rtk/commit/2ed53c7fa26922860af20c445b39cbb66862f180)) -* **curl:** JSON passthrough + IsTerminal gate to prevent invalid JSON output ([02da3d0](https://github.com/rtk-ai/rtk/commit/02da3d070271f800731a94a3249f3feb9dd7c7b8)), closes [#1536](https://github.com/rtk-ai/rtk/issues/1536) [#1282](https://github.com/rtk-ai/rtk/issues/1282) -* dotnet cmd test flakiness ([17ffe62](https://github.com/rtk-ai/rtk/commit/17ffe624d415f05ca4c29e97ca650594778231be)) -* **git:** address review feedback on status state surfacing ([316e65e](https://github.com/rtk-ai/rtk/commit/316e65ef5baa6b926725b8d9a08c8d2ab52c159d)) -* **git:** compact in-progress status state ([cff391e](https://github.com/rtk-ai/rtk/commit/cff391e50b5fa89ae83eed5fd4274c7c444d37f0)) -* **git:** drop state-hint extraction in compact status ([e91dee5](https://github.com/rtk-ai/rtk/commit/e91dee568bdcca0933b137edccc077db9ff006fa)) -* **git:** surface in-progress state in compact `rtk git status` ([017d0f9](https://github.com/rtk-ai/rtk/commit/017d0f9ee6bb799717958d9f3fd3eee4b0e6ca3c)) -* **grep:** adjust the command to fall through if the output would already be as small as possible ([09e1c0a](https://github.com/rtk-ai/rtk/commit/09e1c0ad4b474631b8e058ce69ca2bbd46484c7f)) -* head/tail multi-file rewrite falls back to native command ([#1362](https://github.com/rtk-ai/rtk/issues/1362)) ([f75a10b](https://github.com/rtk-ai/rtk/commit/f75a10b1a2bd824814247a03bded76fa49ddf663)) -* **init-uninstall:** uninstall removes --claude-md artifacts on Windows ([d395f97](https://github.com/rtk-ai/rtk/commit/d395f975c3db7e1cbc825006091e1dcc3867844d)) -* **init-uninstall:** uninstall removes --claude-md artifacts on Windows ([aad0db8](https://github.com/rtk-ai/rtk/commit/aad0db8b5213bd0940ca05f684ecda87de0d93af)) -* **json:** expand char boundary truncation test ([7840030](https://github.com/rtk-ai/rtk/commit/784003055e85b5e6a51f69c2ce0b10662f1b36af)) -* **json:** use char boundary when truncating long string values ([533894a](https://github.com/rtk-ai/rtk/commit/533894a77ec5b8f7374547e994124bcf3a730f0b)) -* **ls:** handle all file types (device, pipe, socket) in ls filter ([e456be1](https://github.com/rtk-ai/rtk/commit/e456be1c1674a32839694446504310a2c16ce7dd)) -* **ls:** handle device files (block, char, pipe, socket) in ls filter ([cac8ce7](https://github.com/rtk-ai/rtk/commit/cac8ce775b695c5837b36ea788ba6812bcae214d)), closes [#844](https://github.com/rtk-ai/rtk/issues/844) -* **ls:** LC_ALL=C + fallback to raw on unrecognized locale ([bf6d4b2](https://github.com/rtk-ai/rtk/commit/bf6d4b2ea22f026d3ec4d909aef81156b0436509)) -* **pnpm:** install don't take a list of packages ([492aa76](https://github.com/rtk-ai/rtk/commit/492aa76ed3842549d2a453becbf2782caba765f1)) - -## [0.38.0](https://github.com/rtk-ai/rtk/compare/v0.37.2...v0.38.0) (2026-04-29) - - -### Features - -* **cicd:** enforce cicd sast & package check ([3bbbb49](https://github.com/rtk-ai/rtk/commit/3bbbb492f33f0e619ab0d1dbce4389ad49e763ae)) -* **gains:** add --reset flag ([e3149cb](https://github.com/rtk-ai/rtk/commit/e3149cb7fbed18eae95f753664ddd8eaaaf6cc39)) -* **glab:** add GitLab CLI (glab) command support ([048f2f9](https://github.com/rtk-ai/rtk/commit/048f2f980bd95c5918f309d1d7ebc096d196f00d)) -* **glab:** add GitLab CLI (glab) command support ([bc31f3f](https://github.com/rtk-ai/rtk/commit/bc31f3f0f39077884e8d52c3508e840b355f682e)), closes [#851](https://github.com/rtk-ai/rtk/issues/851) - - -### Bug Fixes - -* **benchmark:** benchmark capture all fd only stream ([c590bd6](https://github.com/rtk-ai/rtk/commit/c590bd69329bb82608666958c7e06bf169a7d577)) -* **benchmark:** capture all fd for stream cmd benchmark ([e6c2523](https://github.com/rtk-ai/rtk/commit/e6c2523be1180772e40c175e2f9a523d349fb13d)) -* **benchmark:** extract format_diff_changes + remove wrong diff test ([e7ae6bf](https://github.com/rtk-ai/rtk/commit/e7ae6bf018882dba248f151ba4ec4929300b3e36)) -* **cicd:** : no semgrep alert on sh call cicd ([7681daf](https://github.com/rtk-ai/rtk/commit/7681dafc76f164cfad588fe37d9a165dcb476e10)) -* **discover:** also encode '_', '\', and non-ASCII chars in project path slug ([73a05c3](https://github.com/rtk-ai/rtk/commit/73a05c3262b6410cb24370d939c428d1dc0c7a77)), closes [#1457](https://github.com/rtk-ai/rtk/issues/1457) -* **discover:** encode '.' as '-' in project path slug ([2d031f3](https://github.com/rtk-ai/rtk/commit/2d031f32e9ad4452c2cc229c030ea6c0936c8bec)), closes [#1457](https://github.com/rtk-ai/rtk/issues/1457) -* **filters:** benchmark ci update + fix stream + filter quality ([137af04](https://github.com/rtk-ai/rtk/commit/137af0493189a86020da1feaa1de74df92466137)) -* **filters:** benchmark ci update + fix stream filter quality ([88d9f6a](https://github.com/rtk-ai/rtk/commit/88d9f6a0d94fd2b5b3d40c956e966756670a2704)) -* **git:** fix empty output when branch name contains '/' in git diff ([e070226](https://github.com/rtk-ai/rtk/commit/e0702260a94377b6bbec5cb79d91d81cba17b0ec)) -* **git:** fix empty output when branch name contains '/' in git diff ([13188a8](https://github.com/rtk-ai/rtk/commit/13188a88b22f692157b89874f4c76287a0b3ecae)), closes [#1431](https://github.com/rtk-ai/rtk/issues/1431) -* grep false negatives, output mangling, and truncation annotations ([de41533](https://github.com/rtk-ai/rtk/commit/de415335ea069c06370855366945a3704579ee18)) -* **install:** resolve version via redirect to avoid GitHub API rate limits ([5e1a641](https://github.com/rtk-ai/rtk/commit/5e1a64180f094ae456780a78b675f243312089c6)) -* **npm:** regex match end line ([5e84e94](https://github.com/rtk-ai/rtk/commit/5e84e9471736fe58e89094854f4123ecb07c2d3b)) -* **npx:** dispatch unknown tools to npx instead of npm ([2c4569c](https://github.com/rtk-ai/rtk/commit/2c4569caa64d013ad4ada0b7580f9f16d8334c19)), closes [#815](https://github.com/rtk-ai/rtk/issues/815) -* remove wrong cicd benchmark + npm test regex ([7e3690a](https://github.com/rtk-ai/rtk/commit/7e3690a23ab158ca8e1e890650554e20e3a0c17b)) -* **stream:** add semgrep flag for sh tests ([7cfcdbe](https://github.com/rtk-ai/rtk/commit/7cfcdbec8681b15b794b6aef982ccb38feb79fd7)) -* **stream:** add semgrep flag for sh tests ([d327724](https://github.com/rtk-ai/rtk/commit/d327724f814b6875903366db0b0616780b454ad1)) -* **stream:** route to respective fd ([605e335](https://github.com/rtk-ai/rtk/commit/605e335f0546d2ed8554a95e7749a0b494c510e3)) -* **stream:** route to respective fd ([81a1be6](https://github.com/rtk-ai/rtk/commit/81a1be6a744942515347dd296ddcf7d9f126200d)) -* **tracking:** test env path ([70b36b4](https://github.com/rtk-ai/rtk/commit/70b36b4dbc3e147219ad87cf539d073523b86a85)) - -## [0.37.2](https://github.com/rtk-ai/rtk/compare/v0.37.1...v0.37.2) (2026-04-20) - - -### Bug Fixes - -* **discover:** exclude_commands bypass for env-prefix, sub cmd + regex ([ca4c59c](https://github.com/rtk-ai/rtk/commit/ca4c59c230306d310069bed3c0ba930068dc4dc4)) -* **discover:** exclude_commands bypass for env-prefix, sub cmd + regex ([42d3161](https://github.com/rtk-ai/rtk/commit/42d3161872713bc0b20ef49b0714add40c40d5e3)) -* **discover:** word boundary in exclude_commands ([0ea115b](https://github.com/rtk-ai/rtk/commit/0ea115bca5fa66daa69fda2f0eeaaf103346b3a4)) -* **docs:** add missing docs for exclude commands patterns ([2e401ac](https://github.com/rtk-ai/rtk/commit/2e401ac38feec88de8d5e46f0301c8a532b95614)) -* **hooks:** add regression test for windows native ([115e448](https://github.com/rtk-ai/rtk/commit/115e44853b8cdd2d7af3af2b52c9c31e924a45d3)) -* **hooks:** windows use 'rtk hook claude' no fallback ([da3c432](https://github.com/rtk-ai/rtk/commit/da3c432201240f0da9627d8cc6bc70e5b7f8bdfe)) -* **hooks:** windows use 'rtk hook claude' no fallback ([0e29650](https://github.com/rtk-ai/rtk/commit/0e29650e11959730f4c4a2e6d6c0519e14dc8595)) -* **tests:** windows regression test fix path ([13a73dd](https://github.com/rtk-ai/rtk/commit/13a73ddfd78460560a1f5fde94b54b1f848b41b5)) - -## [0.37.1](https://github.com/rtk-ai/rtk/compare/v0.37.0...v0.37.1) (2026-04-18) - - -### Bug Fixes - -* **docs:** user facing docs ([c8d6878](https://github.com/rtk-ai/rtk/commit/c8d68787fb8b31c52125e9fc7ea62e0aa590485f)) - -## [0.37.0](https://github.com/rtk-ai/rtk/compare/v0.36.0...v0.37.0) (2026-04-17) - - -### Features - -* **discover:** handle more npm/npx/pnpm/pnpx patterns ([9e96caa](https://github.com/rtk-ai/rtk/commit/9e96caa0a18a95c84da82ba57716a9d3ef86d0c8)) -* **refacto-core:** binary hook w/ native cmd exec + streaming ([e7b7f9a](https://github.com/rtk-ai/rtk/commit/e7b7f9ab665a0f7303d41d23ad156d24e5e8964e)) - - -### Bug Fixes - -* **docs:** use release please changelog no manual ([7591a14](https://github.com/rtk-ai/rtk/commit/7591a14e4ceb732ab7ca160ac01a852926abe77a)) -* isolate cursor hook tests from local settings (determinist) ([d8ddefe](https://github.com/rtk-ai/rtk/commit/d8ddefe78efe25c35bb2a2f9083f2eacb9dd7274)) -* P0+P1 fixes from pre-merge review of hook engine ([df8e035](https://github.com/rtk-ai/rtk/commit/df8e03558d4d6cc2f5cbac91c63ab1b3b51d3bcd)) -* P0+P1 fixes from pre-merge review of hook engine ([d34389c](https://github.com/rtk-ai/rtk/commit/d34389c3d0936c2b0790e14f450bb50a28a7edf7)) -* rename ship.md to ship/SKILL.md to match develop ([5916ecd](https://github.com/rtk-ai/rtk/commit/5916ecd86fb319c2519a0b4fb2891309833a3bb4)) -* **runner:** preserve fd separation on command failure ([e92d099](https://github.com/rtk-ai/rtk/commit/e92d0993c93f0b732316dfa932d265aeca7488d6)) -* **stream:** missing stderr fields ([a1d46f3](https://github.com/rtk-ai/rtk/commit/a1d46f39c291e3356b9c26a062bde05ba1de591a)) - -## [0.36.0](https://github.com/rtk-ai/rtk/compare/v0.35.0...v0.36.0) (2026-04-13) - - -### Features - -* **benchmark:** add multipass VM integration test suite ([6e7863b](https://github.com/rtk-ai/rtk/commit/6e7863bf313b0d18a47cf0ca2cdaea03cc2ed900)) -* **benchmark:** add multipass VM integration test suite ([d22759b](https://github.com/rtk-ai/rtk/commit/d22759b8c5254ad9c4a455f10cb7de75e92df581)) -* **benchmark:** add Swift ecosystem tests (6 commands + savings) ([1fbb6d9](https://github.com/rtk-ai/rtk/commit/1fbb6d935b4a0d031a7862cba312eebe1303ba9b)) -* **init:** add native support for Kilo Code and Google Antigravity ([d0a3797](https://github.com/rtk-ai/rtk/commit/d0a3797ec580f96948489d1e7c3329ac22a6c4eb)) -* **init:** add support for kilocode and antigravity agents ([66b90f1](https://github.com/rtk-ai/rtk/commit/66b90f1ed3de81acdce61164c068c24ed7ef29db)) -* **pnpm:** Add filter argument support ([2ba8d37](https://github.com/rtk-ai/rtk/commit/2ba8d372df186b4056a3b8906fc25cde8586dd42)) -* **skills:** add /pr-review skill for batch PR review ([21e67a1](https://github.com/rtk-ai/rtk/commit/21e67a1113041b74542d0285e5f74587dfb30b65)) -* **telemetry:** enrich daily ping with gap detection and quality metrics ([644c50f](https://github.com/rtk-ai/rtk/commit/644c50f786e5c567617e7ea907c5f312797b1265)) - - -### Bug Fixes - -* **benchmark:** address PR review feedback ([87ee81f](https://github.com/rtk-ai/rtk/commit/87ee81f08be5e7b1ca79513b1a91925d455f4f5c)) -* **benchmark:** address review feedback from @FlorianBruniaux ([d13c185](https://github.com/rtk-ai/rtk/commit/d13c185aac64d14288b574df44623723a69e7b95)) -* **ccusage:** add --yes flag and warn when falling back to npx ([f68fa00](https://github.com/rtk-ai/rtk/commit/f68fa0087c03d6882993b7b3eaee98e1dbab41b4)) -* **clippy:** show full error blocks instead of truncated headline ([95d9d13](https://github.com/rtk-ai/rtk/commit/95d9d134b0b76d83b6162614b0a79269b2135f40)) -* **clippy:** show full error blocks instead of truncated headline ([f4074f8](https://github.com/rtk-ai/rtk/commit/f4074f898a9b73b72bbcd8b18afab4831dcda406)), closes [#602](https://github.com/rtk-ai/rtk/issues/602) -* **curl:** skip JSON schema conversion for internal/localhost URLs ([577c311](https://github.com/rtk-ai/rtk/commit/577c311ecaaa8ae94f22dbe252152424d4333d04)) -* **discover:** preserve golangci-lint flags in rewrite ([d85303e](https://github.com/rtk-ai/rtk/commit/d85303ec4893deb904260f5dc11b7df906a50c07)) -* **docs:** update TELEMETRY.md to match code after review fixes ([be5c057](https://github.com/rtk-ai/rtk/commit/be5c0576d95566f37f266fd9f92e2a1b263697bd)) -* **find:** include hidden files when pattern targets dotfiles ([#1101](https://github.com/rtk-ai/rtk/issues/1101)) ([dbeeaed](https://github.com/rtk-ai/rtk/commit/dbeeaed16aee79674ec2fd3778b7b11b10b847c6)) -* **git:** re-insert -- separator when clap consumes it from git diff args ([#1215](https://github.com/rtk-ai/rtk/issues/1215)) ([9979c69](https://github.com/rtk-ai/rtk/commit/9979c699307a4adad2c2df0f2bc3b663df653311)) -* **git:** remove -u short alias from --ultra-compact to fix git push -u ([6b76fdb](https://github.com/rtk-ai/rtk/commit/6b76fdb87d7c54cfc2a1b0e6117dd78b8430910b)) -* **golangci-lint:** restore run wrapper and align guidance ([4f4e4d2](https://github.com/rtk-ai/rtk/commit/4f4e4d2b5a3529030fe4089f60d2f4b8740b5d53)) -* **golangci-lint:** support inline global flags before run ([24f2ada](https://github.com/rtk-ai/rtk/commit/24f2adaf8fb541c2564fa7dfb423947932e68fb4)) -* **go:** prevent double-counted failures when test-level fail also triggers package-level fail ([#958](https://github.com/rtk-ai/rtk/issues/958)) ([4fc15ef](https://github.com/rtk-ai/rtk/commit/4fc15ef2c1c80336ffaafa4179db4cee6f39236a)) -* **go:** prevent double-counting failures when package-level fail cascades from test failures ([#958](https://github.com/rtk-ai/rtk/issues/958)) ([9722d5e](https://github.com/rtk-ai/rtk/commit/9722d5ebd8916f9b398bdc01b1102d42ab2b8795)) -* **hooks:** ensure default permission verdict prompts user for confirmation ([40462c0](https://github.com/rtk-ai/rtk/commit/40462c05e66f116928de365a0d271bdfd61cec72)) -* **hooks:** require all segments to match allow rules ([#1213](https://github.com/rtk-ai/rtk/issues/1213)) ([40c9dbc](https://github.com/rtk-ai/rtk/commit/40c9dbc7dbbf9332d6859060765c582a880f0fde)) -* **init:** honor CODEX_HOME for Codex global paths ([d442799](https://github.com/rtk-ai/rtk/commit/d442799e34d522c87a6eb60c2ff373385d201315)) -* **init:** install Codex global instructions in CODEX_HOME ([a257688](https://github.com/rtk-ai/rtk/commit/a2576883a27c5f915ba0ae7883a51006411b3ae5)) -* **json:** rename --schema to --keys-only, closes [#621](https://github.com/rtk-ai/rtk/issues/621) ([c16713a](https://github.com/rtk-ai/rtk/commit/c16713a973b563a6cba283c830b67c8c470e419f)) -* **ls:** filter quality wrong truncation ([aa6317f](https://github.com/rtk-ai/rtk/commit/aa6317fb83a5d9883623a4d3bee7a25bc99dcb4c)) -* **permissions:** glob_matches middle-wildcard matches commands without trailing args ([#1105](https://github.com/rtk-ai/rtk/issues/1105)) ([3db8070](https://github.com/rtk-ai/rtk/commit/3db8070b51b9a312fcca20a8460d3d6259cc38b7)) -* **pnpm:** list command not working ([ba235d8](https://github.com/rtk-ai/rtk/commit/ba235d85974c0a85b25e290a8bb83648800438a6)) -* **pytest:** -q mode summary line not detected ([57502a5](https://github.com/rtk-ai/rtk/commit/57502a5bef1fb56109a57cf2ea7377fd271253a7)) -* report package-level failures (timeouts, signals) in go test summary ([0b1c32b](https://github.com/rtk-ai/rtk/commit/0b1c32b3cc9a3e73418d401d1d481c1611c7ec0b)) -* report package-level failures (timeouts, signals) in go test summary ([c85a387](https://github.com/rtk-ai/rtk/commit/c85a387363e2079234b6141aad26418172c0e61a)), closes [#958](https://github.com/rtk-ai/rtk/issues/958) -* **security:** correct email domain from .dev to .app ([47383e8](https://github.com/rtk-ai/rtk/commit/47383e80197fc56e38f880f33a6b54261b82523c)) -* **tee:** prevent panic on UTF-8 multi-byte truncation boundary ([da486bf](https://github.com/rtk-ai/rtk/commit/da486bf394330c804cd1cd12e4b6835f18de5205)) -* **telemetry:** 7 bugs in enrichment — privacy leak, broken meta_usage, pricing ([15f666d](https://github.com/rtk-ai/rtk/commit/15f666dd8dbd18648cb7bd14a6f9f3cac2f7d10b)) -* **telemetry:** clean code ([8156081](https://github.com/rtk-ai/rtk/commit/81560812610686fa5ca3633c2bf0b79c05eaa7d9)) -* **telemetry:** consent, erasure, auth, docs ([2e4cc4b](https://github.com/rtk-ai/rtk/commit/2e4cc4bb5226444c8c0bfc827baf0c101c3759e8)) -* **telemetry:** non-terminal consent, single config load ([7821e98](https://github.com/rtk-ai/rtk/commit/7821e9872fd1f1ae9b40eb8a4458049869acc36b)) -* **telemetry:** RGPD-compliant, consent gate, erasure, privacy controls ([6a5bc84](https://github.com/rtk-ai/rtk/commit/6a5bc847e06cf6066e6f4aeed5a3ad0803a3649b)) - -## [0.35.0](https://github.com/rtk-ai/rtk/compare/v0.34.3...v0.35.0) (2026-04-06) - - -### Features - -* **aws:** expand CLI filters from 8 to 25 subcommands ([402c48e](https://github.com/rtk-ai/rtk/commit/402c48e66988e638a5b4f4dd193238fc1d0fe18f)) - - -### Bug Fixes - -* **cmd:** read/cat multiple file and consistent behavior ([3f58018](https://github.com/rtk-ai/rtk/commit/3f58018f4af1d7206457929cf80bb4534203c3ee)) -* **docs:** clean some docs + disclaimer ([deda44f](https://github.com/rtk-ai/rtk/commit/deda44f73607981f3d27ecc6341ce927aab34d37)) -* **gh:** pass through gh pr merge instead of canned response ([#938](https://github.com/rtk-ai/rtk/issues/938)) ([8465ca9](https://github.com/rtk-ai/rtk/commit/8465ca953fa9d70dcc971a941c19465d456eb7d4)) -* **gh:** pass through gh pr merge instead of canned response ([#938](https://github.com/rtk-ai/rtk/issues/938)) ([e1f2845](https://github.com/rtk-ai/rtk/commit/e1f2845df06a8d8b8325945dc4940ec5f530e4cc)) -* **git:** inherit stdin for commit and push to preserve SSH signing ([#733](https://github.com/rtk-ai/rtk/issues/733)) ([eefeae4](https://github.com/rtk-ai/rtk/commit/eefeae45656ff2607c3f519c8eae235e3f0fe411)) -* **git:** inherit stdin for commit and push to preserve SSH signing ([#733](https://github.com/rtk-ai/rtk/issues/733)) ([6cee6c6](https://github.com/rtk-ai/rtk/commit/6cee6c60b80f914ed9505e3925d85cadec43ab97)) -* **git:** preserve full diff hunk headers ([62f4452](https://github.com/rtk-ai/rtk/commit/62f445227679f3df293fe35e9b18cc5ab39d7963)) -* **git:** preserve full diff hunk headers ([09b3ff9](https://github.com/rtk-ai/rtk/commit/09b3ff9424e055f5fe25e535e5b60e077f8344f9)) -* **go:** avoid false build errors from download logs ([9c1cf2f](https://github.com/rtk-ai/rtk/commit/9c1cf2f403534fa7874638b1b983c2d7f918a185)) -* **go:** avoid false build errors from download logs ([d44fd3e](https://github.com/rtk-ai/rtk/commit/d44fd3e034208e3bcd59c2c46f7720eec4f10c98)) -* **go:** cover more build failure shapes ([2425ad6](https://github.com/rtk-ai/rtk/commit/2425ad68e5386d19e5ec9ff1ca151a6d2c9a56d3)) -* **go:** preserve failing test location context ([1481bc5](https://github.com/rtk-ai/rtk/commit/1481bc590924031456a6022510275c29c09e330e)) -* **go:** preserve failing test location context ([374fe64](https://github.com/rtk-ai/rtk/commit/374fe64cfbedcd676733973e81a63a6dfecbb1b7)) -* **go:** restore build error coverage ([1177c9c](https://github.com/rtk-ai/rtk/commit/1177c9c873ac63b6c0bcc9e1b664a705baa0ad7a)) -* **grep:** close subprocess stdin to prevent memory leak ([#897](https://github.com/rtk-ai/rtk/issues/897)) ([7217562](https://github.com/rtk-ai/rtk/commit/72175623551f40b581b4a7f6ed966c1e4a9c7358)) -* **grep:** close subprocess stdin to prevent memory leak ([#897](https://github.com/rtk-ai/rtk/issues/897)) ([09979cf](https://github.com/rtk-ai/rtk/commit/09979cf29701a1b775bcac761d24ec0e055d1bec)) -* **hook_check:** detect missing integrations ([9cf9ccc](https://github.com/rtk-ai/rtk/commit/9cf9ccc1ac39f8bba37e932c7d318a3aa7a34ae9)) -* **init:** remove opt-out instruction from telemetry message ([7571c8e](https://github.com/rtk-ai/rtk/commit/7571c8e101c41ee64c51e2bd64697f85f9142423)) -* **init:** remove telemetry info lines from init output ([7dbef2c](https://github.com/rtk-ai/rtk/commit/7dbef2ce00824d26f2057e4c3c76e429e2e23088)) -* **main:** kill zombie processes + path for rtk md ([d16fc6d](https://github.com/rtk-ai/rtk/commit/d16fc6dacbfec912c21522939b15b7bbd9719487)) -* **main:** kill zombie processes + path for rtk md + missing intergrations ([a919335](https://github.com/rtk-ai/rtk/commit/a919335519ed4a5259a212e56407cb312aa99bac)) -* **merge:** changelog conflicts ([d92c5d2](https://github.com/rtk-ai/rtk/commit/d92c5d264a49483c8d6079e04d946a79bc990a74)) -* **proxy:** kill child process on SIGINT/SIGTERM to prevent orphans ([d813919](https://github.com/rtk-ai/rtk/commit/d813919a24546e044e7844fc7ed05fef4ec24033)) -* **proxy:** kill child process on SIGINT/SIGTERM to prevent orphans ([3318510](https://github.com/rtk-ai/rtk/commit/33185101fc122d0c11a25a4e02ac9f3a7dc7e3bb)) -* **review:** address ChildGuard disarm, stdin dedup, hook masking ([d85fe33](https://github.com/rtk-ai/rtk/commit/d85fe3384b87c16fafd25ec7bcadbff6e69f3f1f)) -* **security:** default to ask when no permission rule matches ([#886](https://github.com/rtk-ai/rtk/issues/886)) ([158c745](https://github.com/rtk-ai/rtk/commit/158c74527f6591d372e40a78cd604d73a20649a9)) -* **security:** default to ask when no permission rule matches ([#886](https://github.com/rtk-ai/rtk/issues/886)) ([41a6c6b](https://github.com/rtk-ai/rtk/commit/41a6c6bf6da78a4754794fdc6a1469df2e327920)) -* **tracking:** use std::env::temp_dir() for compatibility (instead of unix tmp) ([e918661](https://github.com/rtk-ai/rtk/commit/e918661440d7b50321f0535032f52c5e87aaf3cb)) - -## [Unreleased] - -### Bug Fixes - -* **git:** remove `-u` short alias from `--ultra-compact` to fix `git push -u` upstream tracking ([#1086](https://github.com/rtk-ai/rtk/issues/1086)) - -## [0.35.0](https://github.com/rtk-ai/rtk/compare/v0.34.3...v0.35.0) (2026-04-06) - - -### Features - -* **aws:** expand CLI filters from 8 to 25 subcommands ([402c48e](https://github.com/rtk-ai/rtk/commit/402c48e66988e638a5b4f4dd193238fc1d0fe18f)) - - -### Bug Fixes - -* **cmd:** read/cat multiple file and consistent behavior ([3f58018](https://github.com/rtk-ai/rtk/commit/3f58018f4af1d7206457929cf80bb4534203c3ee)) -* **docs:** clean some docs + disclaimer ([deda44f](https://github.com/rtk-ai/rtk/commit/deda44f73607981f3d27ecc6341ce927aab34d37)) -* **gh:** pass through gh pr merge instead of canned response ([#938](https://github.com/rtk-ai/rtk/issues/938)) ([8465ca9](https://github.com/rtk-ai/rtk/commit/8465ca953fa9d70dcc971a941c19465d456eb7d4)) -* **gh:** pass through gh pr merge instead of canned response ([#938](https://github.com/rtk-ai/rtk/issues/938)) ([e1f2845](https://github.com/rtk-ai/rtk/commit/e1f2845df06a8d8b8325945dc4940ec5f530e4cc)) -* **git:** inherit stdin for commit and push to preserve SSH signing ([#733](https://github.com/rtk-ai/rtk/issues/733)) ([eefeae4](https://github.com/rtk-ai/rtk/commit/eefeae45656ff2607c3f519c8eae235e3f0fe411)) -* **git:** inherit stdin for commit and push to preserve SSH signing ([#733](https://github.com/rtk-ai/rtk/issues/733)) ([6cee6c6](https://github.com/rtk-ai/rtk/commit/6cee6c60b80f914ed9505e3925d85cadec43ab97)) -* **git:** preserve full diff hunk headers ([62f4452](https://github.com/rtk-ai/rtk/commit/62f445227679f3df293fe35e9b18cc5ab39d7963)) -* **git:** preserve full diff hunk headers ([09b3ff9](https://github.com/rtk-ai/rtk/commit/09b3ff9424e055f5fe25e535e5b60e077f8344f9)) -* **go:** avoid false build errors from download logs ([9c1cf2f](https://github.com/rtk-ai/rtk/commit/9c1cf2f403534fa7874638b1b983c2d7f918a185)) -* **go:** avoid false build errors from download logs ([d44fd3e](https://github.com/rtk-ai/rtk/commit/d44fd3e034208e3bcd59c2c46f7720eec4f10c98)) -* **go:** cover more build failure shapes ([2425ad6](https://github.com/rtk-ai/rtk/commit/2425ad68e5386d19e5ec9ff1ca151a6d2c9a56d3)) -* **go:** preserve failing test location context ([1481bc5](https://github.com/rtk-ai/rtk/commit/1481bc590924031456a6022510275c29c09e330e)) -* **go:** preserve failing test location context ([374fe64](https://github.com/rtk-ai/rtk/commit/374fe64cfbedcd676733973e81a63a6dfecbb1b7)) -* **go:** restore build error coverage ([1177c9c](https://github.com/rtk-ai/rtk/commit/1177c9c873ac63b6c0bcc9e1b664a705baa0ad7a)) -* **grep:** close subprocess stdin to prevent memory leak ([#897](https://github.com/rtk-ai/rtk/issues/897)) ([7217562](https://github.com/rtk-ai/rtk/commit/72175623551f40b581b4a7f6ed966c1e4a9c7358)) -* **grep:** close subprocess stdin to prevent memory leak ([#897](https://github.com/rtk-ai/rtk/issues/897)) ([09979cf](https://github.com/rtk-ai/rtk/commit/09979cf29701a1b775bcac761d24ec0e055d1bec)) -* **hook_check:** detect missing integrations ([9cf9ccc](https://github.com/rtk-ai/rtk/commit/9cf9ccc1ac39f8bba37e932c7d318a3aa7a34ae9)) -* **init:** remove opt-out instruction from telemetry message ([7571c8e](https://github.com/rtk-ai/rtk/commit/7571c8e101c41ee64c51e2bd64697f85f9142423)) -* **init:** remove telemetry info lines from init output ([7dbef2c](https://github.com/rtk-ai/rtk/commit/7dbef2ce00824d26f2057e4c3c76e429e2e23088)) -* **main:** kill zombie processes + path for rtk md ([d16fc6d](https://github.com/rtk-ai/rtk/commit/d16fc6dacbfec912c21522939b15b7bbd9719487)) -* **main:** kill zombie processes + path for rtk md + missing intergrations ([a919335](https://github.com/rtk-ai/rtk/commit/a919335519ed4a5259a212e56407cb312aa99bac)) -* **merge:** changelog conflicts ([d92c5d2](https://github.com/rtk-ai/rtk/commit/d92c5d264a49483c8d6079e04d946a79bc990a74)) -* **proxy:** kill child process on SIGINT/SIGTERM to prevent orphans ([d813919](https://github.com/rtk-ai/rtk/commit/d813919a24546e044e7844fc7ed05fef4ec24033)) -* **proxy:** kill child process on SIGINT/SIGTERM to prevent orphans ([3318510](https://github.com/rtk-ai/rtk/commit/33185101fc122d0c11a25a4e02ac9f3a7dc7e3bb)) -* **review:** address ChildGuard disarm, stdin dedup, hook masking ([d85fe33](https://github.com/rtk-ai/rtk/commit/d85fe3384b87c16fafd25ec7bcadbff6e69f3f1f)) -* **security:** default to ask when no permission rule matches ([#886](https://github.com/rtk-ai/rtk/issues/886)) ([158c745](https://github.com/rtk-ai/rtk/commit/158c74527f6591d372e40a78cd604d73a20649a9)) -* **security:** default to ask when no permission rule matches ([#886](https://github.com/rtk-ai/rtk/issues/886)) ([41a6c6b](https://github.com/rtk-ai/rtk/commit/41a6c6bf6da78a4754794fdc6a1469df2e327920)) -* **tracking:** use std::env::temp_dir() for compatibility (instead of unix tmp) ([e918661](https://github.com/rtk-ai/rtk/commit/e918661440d7b50321f0535032f52c5e87aaf3cb)) - -## [Unreleased] - -### Features - -* **aws:** expand CLI filters from 8 to 25 subcommands — CloudWatch Logs, CloudFormation events, Lambda, IAM, DynamoDB (with type unwrapping), ECS tasks, EC2 security groups, S3API objects, S3 sync/cp, EKS, SQS, Secrets Manager ([#885](https://github.com/rtk-ai/rtk/pull/885)) -* **aws:** add shared runner `run_aws_filtered()` eliminating per-handler boilerplate -* **tee:** add `force_tee_hint()` — truncated output saves full data to file with recovery hint - -## [0.34.3](https://github.com/rtk-ai/rtk/compare/v0.34.2...v0.34.3) (2026-04-02) - - -### Bug Fixes - -* **automod:** add auto discovery for cmds ([234909d](https://github.com/rtk-ai/rtk/commit/234909d2c754ade2fdc939b0a1435a8e34ffc305)) -* **ci:** fix validate-docs.sh broken module count check ([bbe3da6](https://github.com/rtk-ai/rtk/commit/bbe3da642b5fc4b065b13a65647ea0ebf5264e65)) -* **cleaning:** constant extract ([aabc016](https://github.com/rtk-ai/rtk/commit/aabc0167bc013fd2d0c61a687580f6e69305500a)) -* **cmds:** migrate remaining exit_code to exit_code_from_output ([ba9fa34](https://github.com/rtk-ai/rtk/commit/ba9fa345f3d1d14bd0af236ec9aa8a9a0e5581d6)) -* **cmds:** more covering for run_filtered ([e48485a](https://github.com/rtk-ai/rtk/commit/e48485adc6a33d12b70664598020595cf7dfcd7e)) -* **docs:** add documentation ([2f7278a](https://github.com/rtk-ai/rtk/commit/2f7278ac5992bf2e84b763fb05642d89900ba495)) -* **docs:** add maintainers docs ([14265b4](https://github.com/rtk-ai/rtk/commit/14265b48c3a15e459a31da11250a51ab5830a508)) -* **refacto-p1:** unified cmds execution flow (+ rm dead code) ([75bd607](https://github.com/rtk-ai/rtk/commit/75bd607d55235f313855f5fe8c9eceafd73700a7)) -* **refacto-p2:** more standardize ([47a76ea](https://github.com/rtk-ai/rtk/commit/47a76ea35ed2fe02a3600792163f727fa3a94ff2)) -* **refacto-p2:** more standardize ([92c671a](https://github.com/rtk-ai/rtk/commit/92c671a175a5e2bf09720fd1a8591140bcb473a0)) -* **refacto:** wrappers for standardization, exit codes lexer tokenizer, constants, code clean ([bff0258](https://github.com/rtk-ai/rtk/commit/bff02584243f1b73418418b0c05365acf56fbb36)) -* **registry:** quoted env prefix + inline regex cleanup + routing docs ([f3217a4](https://github.com/rtk-ai/rtk/commit/f3217a467b543a3181605b257162f2b3ab5d5df0)) -* **review:** address PR [#910](https://github.com/rtk-ai/rtk/issues/910) review feedback ([0a8b8fd](https://github.com/rtk-ai/rtk/commit/0a8b8fd0693fa504f376146cbbcafe9ddf4632c8)) -* **review:** PR [#934](https://github.com/rtk-ai/rtk/issues/934) ([5bd35a3](https://github.com/rtk-ai/rtk/commit/5bd35a33ad6abe5278749726bed19912664531c2)) -* **review:** PR [#934](https://github.com/rtk-ai/rtk/issues/934) ([bae7930](https://github.com/rtk-ai/rtk/commit/bae79301194bbb48d1cbb39554096c3225f7cb73)) -* **rules:** add wc RtkRule with pattern field for develop compat ([d75e864](https://github.com/rtk-ai/rtk/commit/d75e864f20451a5e17918c75f2ea32672f65e1f4)) -* **standardize:** git+kube sub wrappers run_filtered ([7fd221f](https://github.com/rtk-ai/rtk/commit/7fd221f44660bcf411aa333d2c35a49ff89e7961)) -* **standardize:** merge pattern into rues ([08aabb9](https://github.com/rtk-ai/rtk/commit/08aabb95c3ae6e0b734f696264e1e1a8c0f0b22e)) - -## [0.34.2](https://github.com/rtk-ai/rtk/compare/v0.34.1...v0.34.2) (2026-03-30) - - -### Bug Fixes - -* **emots:** replace 📊 with "Summary:" ([495a152](https://github.com/rtk-ai/rtk/commit/495a152059feabc7b516b96e804757608b87a10a)) -* **refacto-codebase:** technical docs & sub folders ([927daef](https://github.com/rtk-ai/rtk/commit/927daef49b8f771d195201d196378e27e0ee8a2b)) - -## [0.34.1](https://github.com/rtk-ai/rtk/compare/v0.34.0...v0.34.1) (2026-03-28) - - -### Bug Fixes - -* **security:** missing toml pkg ([51f9c88](https://github.com/rtk-ai/rtk/commit/51f9c888b81169309df92f7fa3a6f705d44adcab)) -* **security:** salt device hash for telemetry ([32fdbbb](https://github.com/rtk-ai/rtk/commit/32fdbbbb6923c70d343fab14b4b0ce70424e610f)) -* **security:** set 0600 permissions on salt file ([5eae11d](https://github.com/rtk-ai/rtk/commit/5eae11d16410dc4ff26e97672e5367b14efaab76)) -* **telemetry:** cache salt in-process ([22dc059](https://github.com/rtk-ai/rtk/commit/22dc059310b0408adedc2d1228de339e16ea6c0a)) -* **telemetry:** docs + real info from "rtk init -g" ([33195cc](https://github.com/rtk-ai/rtk/commit/33195cc686318ddcca54edfdd1215bd9fd28f891)) -* **telemetry:** hash + salt ([92996b1](https://github.com/rtk-ai/rtk/commit/92996b127257eae16d3e17179592b2899f19254f)) - -## [0.34.0](https://github.com/rtk-ai/rtk/compare/v0.33.1...v0.34.0) (2026-03-26) - - -### Features - -* **init:** add --copilot flag for GitHub Copilot integration ([9e19aac](https://github.com/rtk-ai/rtk/commit/9e19aac75e790ecbfd1dc5b2d01786f6b9edf506)), closes [#823](https://github.com/rtk-ai/rtk/issues/823) - - -### Bug Fixes - -* **diff:** correct truncation overflow count in condense_unified_diff ([5399f83](https://github.com/rtk-ai/rtk/commit/5399f836a5c642121f0f6e7812ff4131d84d0509)) -* **diff:** never truncate diff content — show all changes in full ([80fc29a](https://github.com/rtk-ai/rtk/commit/80fc29a839f51ef605474037e1a8fd86b4aac05a)), closes [#827](https://github.com/rtk-ai/rtk/issues/827) -* **git:** replace vague truncation markers with exact counts ([185fb97](https://github.com/rtk-ai/rtk/commit/185fb97061517922ea5844d8c6008f2eb86fd55d)) -* **merge:** resolve conflict with develop in diff_cmd.rs ([6a5ae14](https://github.com/rtk-ai/rtk/commit/6a5ae1484b32c38bd99baca925175ae610e3d1e3)) -* **read:** default to no filtering — show full file content ([5e0f3ba](https://github.com/rtk-ai/rtk/commit/5e0f3ba774eab52f8ca2ac603e2ae4eae79b2edc)), closes [#822](https://github.com/rtk-ai/rtk/issues/822) -* **read:** detect binary files and prevent empty output on filter failure ([8886c14](https://github.com/rtk-ai/rtk/commit/8886c14c9cf97fb4413efec3be8e50fdb84824e9)), closes [#822](https://github.com/rtk-ai/rtk/issues/822) -* rewrite swift test commands ([599ad25](https://github.com/rtk-ai/rtk/commit/599ad25deb0f8dc9ecab37f4bbe26324dac07b2e)) -* truncation accuracy + Copilot init + binary file detection ([966bcbe](https://github.com/rtk-ai/rtk/commit/966bcbe638be18bbaba4298df985804643f82c85)) -* **truncation:** accurate overflow counts and omission indicators ([58a9633](https://github.com/rtk-ai/rtk/commit/58a963347467613d48db05ad56bc8f1f3a06b65d)) - -## [Unreleased] - -### Bug Fixes - -* **wc:** `wc` filter was never invoked by the hook — removed `"wc "` from `IGNORED_PREFIXES` and added registry entry so `wc` commands are rewritten to `rtk wc` -* **diff:** correct truncation overflow count in condense_unified_diff ([#833](https://github.com/rtk-ai/rtk/pull/833)) ([5399f83](https://github.com/rtk-ai/rtk/commit/5399f83)) -* **git:** replace vague truncation markers with exact counts in log and grep output ([#833](https://github.com/rtk-ai/rtk/pull/833)) ([185fb97](https://github.com/rtk-ai/rtk/commit/185fb97)) - -## [0.33.1](https://github.com/rtk-ai/rtk/compare/v0.33.0...v0.33.1) (2026-03-25) - - -### Bug Fixes - -* **cicd:** dev- prefix for pre-release tags ([522bd64](https://github.com/rtk-ai/rtk/commit/522bd648c8cae41f6cadedcd40a96d879c6ecf0a)) -* **cicd:** use dev- prefix for pre-release tags ([9c21275](https://github.com/rtk-ai/rtk/commit/9c212752fc0401820f8665198f00882684496175)) -* **cicd:** use dev- prefix for pre-release tags to avoid polluting release-please ([32c67e0](https://github.com/rtk-ai/rtk/commit/32c67e01326374f0365602f61542a3639a8f121b)) -* hook security + stderr redirects + version bump ([#807](https://github.com/rtk-ai/rtk/issues/807)) ([0649e97](https://github.com/rtk-ai/rtk/commit/0649e974fb8f27778ef0d22aa97905d9ebc8f03c)) -* **hook:** respect Claude Code deny/ask permission rules on rewrite ([a051a6f](https://github.com/rtk-ai/rtk/commit/a051a6f5e56c7ee59375a365580bced634e29c02)) -* strip trailing stderr redirects before rewrite matching ([#530](https://github.com/rtk-ai/rtk/issues/530)) ([edd9c02](https://github.com/rtk-ai/rtk/commit/edd9c02e892b297a7e349031b61ef971c982b53d)) -* strip trailing stderr redirects before rewrite matching ([#530](https://github.com/rtk-ai/rtk/issues/530)) ([36a6f48](https://github.com/rtk-ai/rtk/commit/36a6f482296d6fc85f8116040a16de2e128733f8)) - -## [0.33.0-rc.54](https://github.com/rtk-ai/rtk/compare/v0.32.0-rc.54...v0.33.0-rc.54) (2026-03-24) - - -### Features - -* **ruby:** add Ruby on Rails support (rspec, rubocop, rake, bundle) ([#724](https://github.com/rtk-ai/rtk/issues/724)) ([15bc0f8](https://github.com/rtk-ai/rtk/commit/15bc0f8d6e135371688d5fd42decc6d8a99454f0)) - - -### Bug Fixes - -* add telemetry documentation and init notice ([#640](https://github.com/rtk-ai/rtk/issues/640)) ([#788](https://github.com/rtk-ai/rtk/issues/788)) ([0eecee5](https://github.com/rtk-ai/rtk/commit/0eecee5bf35ffd8b13f36a59ec39bd52626948d3)) -* **cargo:** preserve test compile diagnostics ([97b6878](https://github.com/rtk-ai/rtk/commit/97b68783f50d209c2c599ae42cc638520749e668)) -* **cicd:** explicit fetch tag ([3b94b60](https://github.com/rtk-ai/rtk/commit/3b94b602ed24b9ecec597ce001e59f325caaadd4)) -* **cicd:** gete release like tag for pre-release ([53bc81e](https://github.com/rtk-ai/rtk/commit/53bc81e9e6d3d0876fb1a23dbf6f08bc074b68be)) -* **cicd:** issue 668 - pre release tag ([200af43](https://github.com/rtk-ai/rtk/commit/200af436d48dd2539cb00652b082f25c57873c9c)) -* **cicd:** missing doc ([8657494](https://github.com/rtk-ai/rtk/commit/865749438e67f6da7f719d054bf377d857925ad3)) -* **cicd:** pre-release correct tag ([1536667](https://github.com/rtk-ai/rtk/commit/15366678adeece701f38e91204128b070c0e3fc4)) -* **dotnet:** TRX injection for Microsoft.Testing.Platform projects ([8eefef1](https://github.com/rtk-ai/rtk/commit/8eefef1b496035ce898effc5446e6851084d6fa4)) -* **formatter:** show full error message for test failures ([#690](https://github.com/rtk-ai/rtk/issues/690)) ([dc6b026](https://github.com/rtk-ai/rtk/commit/dc6b0260ab4c1bdbccb4b775d879eb473b212c21)) -* **formatter:** show full error message for test failures ([#690](https://github.com/rtk-ai/rtk/issues/690)) ([f7b09fc](https://github.com/rtk-ai/rtk/commit/f7b09fc86a693acf2b52954215ff0c4e6c5d03f9)) -* **gh:** passthrough --comments flag in issue/pr view ([75cd223](https://github.com/rtk-ai/rtk/commit/75cd2232e274f898d8a335ba866fc507ce64b949)) -* **gh:** passthrough --comments flag in issue/pr view ([fdeb09f](https://github.com/rtk-ai/rtk/commit/fdeb09fb93564e795711e9a531d2e2e20187c3a7)), closes [#720](https://github.com/rtk-ai/rtk/issues/720) -* **gh:** skip compact_diff for --name-only/--stat flags in pr diff ([2ef0690](https://github.com/rtk-ai/rtk/commit/2ef0690767eb733c705e4de56d02c64696a4acc6)), closes [#730](https://github.com/rtk-ai/rtk/issues/730) -* **gh:** skip compact_diff for --name-only/--stat in pr diff ([c576249](https://github.com/rtk-ai/rtk/commit/c57624931a96181f869645817fdd96bc056da044)) -* **golangci-lint:** add v2 compatibility with runtime version detection ([95a4961](https://github.com/rtk-ai/rtk/commit/95a4961e4aa3ba5307b3dfad246c6168c4caeab8)) -* **golangci:** use resolved_command for version detection, move test fixture to file ([6aa5e90](https://github.com/rtk-ai/rtk/commit/6aa5e90dc466f87c88a2401b4eb2aa0f323379f4)) -* increase signal in git diff, git log, and json filters ([#621](https://github.com/rtk-ai/rtk/issues/621)) ([#708](https://github.com/rtk-ai/rtk/issues/708)) ([4edc3fc](https://github.com/rtk-ai/rtk/commit/4edc3fc0838e25ee6d1754c7e987b5507742f600)) -* **playwright:** add tee_and_hint pass-through on failure ([#690](https://github.com/rtk-ai/rtk/issues/690)) ([b4ccf04](https://github.com/rtk-ai/rtk/commit/b4ccf046f59ce6ed1396e4d8c46f8a35152d6d09)) -* preserve cargo test compile diagnostics ([15d5beb](https://github.com/rtk-ai/rtk/commit/15d5beb9f70caf1f84e9b506faaf840c70c1cf4e)) -* **ruby:** use rails test for positional file args in rtk rake ([ec92c43](https://github.com/rtk-ai/rtk/commit/ec92c43f231eb2321a4b423b0eb8487f98161aac)) -* **ruby:** use rails test for positional file args in rtk rake ([138e914](https://github.com/rtk-ai/rtk/commit/138e91411b4802e445a97429056cca73282d09e1)) -* update Discord invite link ([#711](https://github.com/rtk-ai/rtk/issues/711)) ([#786](https://github.com/rtk-ai/rtk/issues/786)) ([af56573](https://github.com/rtk-ai/rtk/commit/af56573ae2b234123e4685fd945980e644f40fa3)) - -## [Unreleased] - -### Bug Fixes - -* **hook:** respect Claude Code deny/ask permission rules on rewrite — hook now checks settings.json before rewriting commands, preventing bypass of user-configured deny/ask permissions -* **git:** replace symbol prefixes (`* branch`, `+ Staged:`, `~ Modified:`, `? Untracked:`) with plain lowercase labels (`branch:`, `staged:`, `modified:`, `untracked:`) in git status output -* **ruby:** use `rails test` instead of `rake test` when positional file args are passed — `rake test` ignores positional files and only supports `TEST=path` - -### Features - -* **ruby:** add RSpec test runner filter with JSON parsing and text fallback (60%+ reduction) -* **ruby:** add RuboCop linter filter with JSON parsing, grouped by cop/severity (60%+ reduction) -* **ruby:** add Minitest filter for `rake test` / `rails test` with state machine parser (85-90% reduction) -* **ruby:** add TOML filter for `bundle install/update` — strip `Using` lines (90%+ reduction) -* **ruby:** add `ruby_exec()` shared utility for auto-detecting `bundle exec` when Gemfile exists -* **ruby:** add discover/rewrite rules for rake, rails, rspec, rubocop, and bundle commands - -### Bug Fixes - -* **cargo:** preserve compile diagnostics when `cargo test` fails before any test suites run -## [0.31.0](https://github.com/rtk-ai/rtk/compare/v0.30.1...v0.31.0) (2026-03-19) - - -### Features - -* 9-tool AI agent support + emoji removal ([#704](https://github.com/rtk-ai/rtk/issues/704)) ([737dada](https://github.com/rtk-ai/rtk/commit/737dada4a56c0d7a482cc438e7280340d634f75d)) - -## [0.30.1](https://github.com/rtk-ai/rtk/compare/v0.30.0...v0.30.1) (2026-03-18) - - -### Bug Fixes - -* remove all decorative emojis from CLI output ([#687](https://github.com/rtk-ai/rtk/issues/687)) ([#686](https://github.com/rtk-ai/rtk/issues/686)) ([4792008](https://github.com/rtk-ai/rtk/commit/4792008fc15553cbb9aeaa602f773a5f8f7f7afe)) - -## [0.30.0](https://github.com/rtk-ai/rtk/compare/v0.29.0...v0.30.0) (2026-03-16) - - -### Features - -* add rtk session command for adoption overview ([be67d66](https://github.com/rtk-ai/rtk/commit/be67d660100c06a0751c08d943dc884ad5bff6a3)) -* add rtk session command for adoption overview ([12d44c4](https://github.com/rtk-ai/rtk/commit/12d44c4068d7d4f65d5bd7551af29ab5a2352ed1)), closes [#487](https://github.com/rtk-ai/rtk/issues/487) -* add worktree slash commands for isolated development ([#364](https://github.com/rtk-ai/rtk/issues/364)) ([ab83e79](https://github.com/rtk-ai/rtk/commit/ab83e7933ebc26ca76f843d33285729875efb913)) -* Claude Code tooling — 2 agents, 7 commands, 2 rules, 4 skills ([#491](https://github.com/rtk-ai/rtk/issues/491)) ([7b7a5ae](https://github.com/rtk-ai/rtk/commit/7b7a5ae4b6d23fbb882ed7d5e815e2ed0672c46c)) - - -### Bug Fixes - -* 6 critical bugs — exit codes, unwrap, lazy regex ([#626](https://github.com/rtk-ai/rtk/issues/626)) ([3005ebd](https://github.com/rtk-ai/rtk/commit/3005ebd0ad07912ae919687f6d3d49482aabaeac)) -* align 7 TOML filter tests with on_empty behavior ([04ed6d8](https://github.com/rtk-ai/rtk/commit/04ed6d8c314dcbf86b147903b5a7f1cd956dc980)) -* align 7 TOML filter tests with on_empty behavior ([9a499b9](https://github.com/rtk-ai/rtk/commit/9a499b9714e97a553d5603680ab1f843034acf28)) -* **cicd-docs:** add agent reviewer + some contribute guidelines ([de710f4](https://github.com/rtk-ai/rtk/commit/de710f4ea30c333130c46f8a2e2c5b6b9edd4889)) -* **cicd-docs:** some logs to understand what is happening when check docs ([191ea9a](https://github.com/rtk-ai/rtk/commit/191ea9af9f99ee78d74385fe1952ce83045e4afe)) -* **cicd:** Clean cicd, rework depends and add pre-release ([d24a765](https://github.com/rtk-ai/rtk/commit/d24a7650e26aca89224a3ec5d263f1ce7c7121d6)) -* **cicd:** Clean cicd, rework depends and add pre-release ([6303e95](https://github.com/rtk-ai/rtk/commit/6303e9530a379a8e3939e6c122ab4cf07cb16751)) -* **cicd:** clippy - do not treat warn as error ([5da5db2](https://github.com/rtk-ai/rtk/commit/5da5db222d9927394995ccaeb3afc103e80c22bd)) -* failing context for doc analyze -> cat from files ([c6b7db2](https://github.com/rtk-ai/rtk/commit/c6b7db2e5a6cd9a05262e934b4fc7a44c699c3b0)) -* git log --oneline regression drops commits ([#619](https://github.com/rtk-ai/rtk/issues/619)) ([8e85d67](https://github.com/rtk-ai/rtk/commit/8e85d676d78b12d2c421bb892f93971fc222fb39)) -* improve adoption metric by detecting hook-rewritten commands ([eb8a2c4](https://github.com/rtk-ai/rtk/commit/eb8a2c4a71072870fca4b64e90189a4453acff84)) -* normalize binlogs CRLF ([5344af9](https://github.com/rtk-ai/rtk/commit/5344af9a51f06b5dc42692e42c948ff11a3173c6)) -* preserve commit body in git log output ([e189bbb](https://github.com/rtk-ai/rtk/commit/e189bbbe749120eda4d98a2130937269d8c0e92a)) -* preserve first line of commit body in git log output ([c3416eb](https://github.com/rtk-ai/rtk/commit/c3416eb45f2f97297ec149d296a6a500697d302b)) -* remove version check from validate-docs CI ([#476](https://github.com/rtk-ai/rtk/issues/476)) ([#543](https://github.com/rtk-ai/rtk/issues/543)) ([6e61c24](https://github.com/rtk-ai/rtk/commit/6e61c2447cc03af94220ce6ce83686f155e18086)) -* split chained commands in adoption metric ([127f85c](https://github.com/rtk-ai/rtk/commit/127f85c02efd52a64e461005fa142d05f81615f8)) -* support git -C <path> in rewrite registry ([c916bab](https://github.com/rtk-ai/rtk/commit/c916bab33ae9760b234fd720c944a849141f0d2e)), closes [#555](https://github.com/rtk-ai/rtk/issues/555) -* test-all.sh aborts when gt not installed ([#500](https://github.com/rtk-ai/rtk/issues/500)) ([#544](https://github.com/rtk-ai/rtk/issues/544)) ([26f5473](https://github.com/rtk-ai/rtk/commit/26f547371798ad32aed3569965303bc4857789ed)) -* trust boundary followup — TOML key typo + missing meta commands ([#625](https://github.com/rtk-ai/rtk/issues/625)) ([8d8e188](https://github.com/rtk-ai/rtk/commit/8d8e188705e5784829693a83b2076d6118154764)) -* windows path fix for git tests ([0a904e2](https://github.com/rtk-ai/rtk/commit/0a904e264d58f8f4b5f10e37ec3b11f717458fe0)) - -## [0.29.0](https://github.com/rtk-ai/rtk/compare/v0.28.2...v0.29.0) (2026-03-12) - - -### Features - -* rewrite engine, OpenCode support, hook system improvements ([#539](https://github.com/rtk-ai/rtk/issues/539)) ([c1de10d](https://github.com/rtk-ai/rtk/commit/c1de10d94c0a35f825b71713e2db4624310c03d1)) - -## [0.28.2](https://github.com/rtk-ai/rtk/compare/v0.28.1...v0.28.2) (2026-03-10) - - -### Bug Fixes - -* add tokens_saved to telemetry payload ([#471](https://github.com/rtk-ai/rtk/issues/471)) ([#472](https://github.com/rtk-ai/rtk/issues/472)) ([f8b7d52](https://github.com/rtk-ai/rtk/commit/f8b7d52d2d25d09a44f391576bad6a7b271f1f8c)) - -## [0.28.1](https://github.com/rtk-ai/rtk/compare/v0.28.0...v0.28.1) (2026-03-10) - - -### Bug Fixes - -* 4 critical bugs + telemetry enrichment ([#462](https://github.com/rtk-ai/rtk/issues/462)) ([7d76af8](https://github.com/rtk-ai/rtk/commit/7d76af84b95e0f040e8b91a154edb89f80e5c380)) -* restore lost telemetry install_method enrichment ([#469](https://github.com/rtk-ai/rtk/issues/469)) ([0c5cde9](https://github.com/rtk-ai/rtk/commit/0c5cde9ec234a2b7b0376adbcb78f2be48a98e86)) - -## [0.28.0](https://github.com/rtk-ai/rtk/compare/v0.27.2...v0.28.0) (2026-03-10) - - -### Features - -* **gt:** add Graphite CLI support ([#290](https://github.com/rtk-ai/rtk/issues/290)) ([7fbc4ef](https://github.com/rtk-ai/rtk/commit/7fbc4ef4b553d5e61feeb6e73d8f6a96b6df3dd9)) -* TOML Part 1 — filter DSL engine + 14 built-in filters ([#349](https://github.com/rtk-ai/rtk/issues/349)) ([adda253](https://github.com/rtk-ai/rtk/commit/adda2537be1fe69625ac280f15e8c8067d08c711)) -* TOML Part 2 — user-global config, shadow warning, rtk init templates, 4 new built-in filters ([#351](https://github.com/rtk-ai/rtk/issues/351)) ([926e6a0](https://github.com/rtk-ai/rtk/commit/926e6a0dd4512c4cbb0f5ac133e60cb6134a3174)) -* TOML Part 3 — 15 additional built-in filters (ping, rsync, dotnet, swift, shellcheck, hadolint, poetry, composer, brew, df, ps, systemctl, yamllint, markdownlint, uv) ([#386](https://github.com/rtk-ai/rtk/issues/386)) ([b71a8d2](https://github.com/rtk-ai/rtk/commit/b71a8d24e2dbd3ff9bb423c849638bfa23830c0b)) - -## [0.27.2](https://github.com/rtk-ai/rtk/compare/v0.27.1...v0.27.2) (2026-03-06) - - -### Bug Fixes - -* gh pr edit/comment pass correct subcommand to gh ([#332](https://github.com/rtk-ai/rtk/issues/332)) ([799f085](https://github.com/rtk-ai/rtk/commit/799f0856e4547318230fe150a43f50ab82e1cf03)) -* pass through -R/--repo flag in gh view commands ([#328](https://github.com/rtk-ai/rtk/issues/328)) ([0a1bcb0](https://github.com/rtk-ai/rtk/commit/0a1bcb05e5737311211369dcb92b3f756a6230c6)), closes [#223](https://github.com/rtk-ai/rtk/issues/223) -* reduce gh diff / git diff / gh api truncation ([#354](https://github.com/rtk-ai/rtk/issues/354)) ([#370](https://github.com/rtk-ai/rtk/issues/370)) ([e356c12](https://github.com/rtk-ai/rtk/commit/e356c1280da9896195d0dff91e152c5f20347a65)) -* strip npx/bunx/pnpm prefixes in lint linter detection ([#186](https://github.com/rtk-ai/rtk/issues/186)) ([#366](https://github.com/rtk-ai/rtk/issues/366)) ([27b35d8](https://github.com/rtk-ai/rtk/commit/27b35d84a341622aa4bf686c2ce8867f8feeb742)) - -## [0.27.1](https://github.com/rtk-ai/rtk/compare/v0.27.0...v0.27.1) (2026-03-06) - - -### Bug Fixes - -* only rewrite docker compose ps/logs/build, skip unsupported subcommands ([#336](https://github.com/rtk-ai/rtk/issues/336)) ([#363](https://github.com/rtk-ai/rtk/issues/363)) ([dbc9503](https://github.com/rtk-ai/rtk/commit/dbc950395e31b4b0bc48710dc52ad01d4d73f9ba)) -* preserve -- separator for cargo commands and silence fallback ([#326](https://github.com/rtk-ai/rtk/issues/326)) ([45f9344](https://github.com/rtk-ai/rtk/commit/45f9344f033d27bc370ff54c4fc0c61e52446076)), closes [#286](https://github.com/rtk-ai/rtk/issues/286) [#287](https://github.com/rtk-ai/rtk/issues/287) -* prettier false positive when not installed ([#221](https://github.com/rtk-ai/rtk/issues/221)) ([#359](https://github.com/rtk-ai/rtk/issues/359)) ([85b0b3e](https://github.com/rtk-ai/rtk/commit/85b0b3eb0bad9cbacdc32d2e9ba525728acd7cbe)) -* support git commit -am, --amend and other flags ([#327](https://github.com/rtk-ai/rtk/issues/327)) ([#360](https://github.com/rtk-ai/rtk/issues/360)) ([409aed6](https://github.com/rtk-ai/rtk/commit/409aed6dbcdd7cac2a48ec5655e6f1fd8d5248e3)) - -## [0.27.0](https://github.com/rtk-ai/rtk/compare/v0.26.0...v0.27.0) (2026-03-05) - - -### Features - -* warn when installed hook is outdated ([#344](https://github.com/rtk-ai/rtk/issues/344)) ([#350](https://github.com/rtk-ai/rtk/issues/350)) ([3141fec](https://github.com/rtk-ai/rtk/commit/3141fecf958af5ae98c232543b913f3ca388254f)) - - -### Bug Fixes - -* bugs [#196](https://github.com/rtk-ai/rtk/issues/196) [#344](https://github.com/rtk-ai/rtk/issues/344) [#345](https://github.com/rtk-ai/rtk/issues/345) [#346](https://github.com/rtk-ai/rtk/issues/346) [#347](https://github.com/rtk-ai/rtk/issues/347) — gh --json, hook check, RTK_DISABLED, 2>&1, json TOML ([8953af0](https://github.com/rtk-ai/rtk/commit/8953af0fc06759b37f16743ef383af0a52af2bed)) -* RTK_DISABLED ignored, 2>&1 broken, json TOML error ([#345](https://github.com/rtk-ai/rtk/issues/345), [#346](https://github.com/rtk-ai/rtk/issues/346), [#347](https://github.com/rtk-ai/rtk/issues/347)) ([6c13d23](https://github.com/rtk-ai/rtk/commit/6c13d234364d314f53b6698c282a621019635fd6)) -* skip rewrite for gh --json/--jq/--template ([#196](https://github.com/rtk-ai/rtk/issues/196)) ([079ee9a](https://github.com/rtk-ai/rtk/commit/079ee9a4ea868ecf4e7beffcbc681ca1ba8b165c)) - -## [0.26.0](https://github.com/rtk-ai/rtk/compare/v0.25.0...v0.26.0) (2026-03-05) - - -### Features - -* add Claude Code skills for PR and issue triage ([#343](https://github.com/rtk-ai/rtk/issues/343)) ([6ad6ffe](https://github.com/rtk-ai/rtk/commit/6ad6ffeccee9b622013f8e1357b6ca4c94aacb59)) -* anonymous telemetry ping (1/day, opt-out) ([#334](https://github.com/rtk-ai/rtk/issues/334)) ([baff6a2](https://github.com/rtk-ai/rtk/commit/baff6a2334b155c0d68f38dba85bd8d6fe9e20af)) - - -### Bug Fixes - -* curl JSON size guard ([#297](https://github.com/rtk-ai/rtk/issues/297)) + exclude_commands config ([#243](https://github.com/rtk-ai/rtk/issues/243)) ([#342](https://github.com/rtk-ai/rtk/issues/342)) ([a8d6106](https://github.com/rtk-ai/rtk/commit/a8d6106f736e049013ecb77f0f413167266dd40e)) - -## [Unreleased] - -### Features - -* **toml-dsl:** declarative TOML filter engine — add command filters without writing Rust ([#299](https://github.com/rtk-ai/rtk/issues/299)) - * 8 primitives: `strip_ansi`, `replace`, `match_output`, `strip/keep_lines_matching`, `truncate_lines_at`, `head/tail_lines`, `max_lines`, `on_empty` - * lookup chain: `.rtk/filters.toml` (project-local) → `~/.config/rtk/filters.toml` (user-global) → built-in filters - * `RTK_NO_TOML=1` bypass, `RTK_TOML_DEBUG=1` debug mode - * shadow warning when a TOML filter's match_command overlaps a Rust-handled command - * `rtk init` generates commented filter templates at both project and global level - * `rtk verify` command with `--require-all` for inline test validation - * 18 built-in filters: `tofu-plan/init/validate/fmt` ([#240](https://github.com/rtk-ai/rtk/issues/240)), `du` ([#284](https://github.com/rtk-ai/rtk/issues/284)), `fail2ban-client` ([#281](https://github.com/rtk-ai/rtk/issues/281)), `iptables` ([#282](https://github.com/rtk-ai/rtk/issues/282)), `mix-format/compile` ([#310](https://github.com/rtk-ai/rtk/issues/310)), `shopify-theme` ([#280](https://github.com/rtk-ai/rtk/issues/280)), `pio-run` ([#231](https://github.com/rtk-ai/rtk/issues/231)), `mvn-build` ([#338](https://github.com/rtk-ai/rtk/issues/338)), `pre-commit`, `helm`, `gcloud`, `ansible-playbook` -* **hooks:** `exclude_commands` config — exclude specific commands from auto-rewrite ([#243](https://github.com/rtk-ai/rtk/issues/243)) - -### Bug Fixes - -* **cargo clippy:** include actionable error details in compact output instead of summary-only counts ([#602](https://github.com/rtk-ai/rtk/issues/602)) -* **curl:** skip JSON schema replacement when schema is larger than original payload ([#297](https://github.com/rtk-ai/rtk/issues/297)) -* **init:** `rtk init -g --uninstall` now removes `` block from CLAUDE.md ([#384](https://github.com/rtk-ai/rtk/issues/384)) -* **toml-dsl:** fix regex overmatch on `tofu-plan/init/validate/fmt` and `mix-format/compile` — add `(\s|$)` word boundary to prevent matching subcommands (e.g. `tofu planet`, `mix formats`) ([#349](https://github.com/rtk-ai/rtk/issues/349)) -* **toml-dsl:** remove 3 dead built-in filters (`docker-inspect`, `docker-compose-ps`, `pnpm-build`) — Clap routes these commands before `run_fallback`, so the TOML filters never fire ([#351](https://github.com/rtk-ai/rtk/issues/351)) -* **toml-dsl:** `uv-sync` — remove `Resolved` short-circuit; it fires before the package list is printed, hiding installed packages ([#386](https://github.com/rtk-ai/rtk/issues/386)) -* **toml-dsl:** `dotnet-build` — short-circuit only when both warning and error counts are zero; builds with warnings now pass through ([#386](https://github.com/rtk-ai/rtk/issues/386)) -* **toml-dsl:** `poetry-install` — support Poetry 2.x bullet syntax (`•`) and `No changes.` up-to-date message ([#386](https://github.com/rtk-ai/rtk/issues/386)) -* **toml-dsl:** `ping` — add Windows format support (`Pinging` header, `Reply from` per-packet lines) ([#386](https://github.com/rtk-ai/rtk/issues/386)) - -## [0.25.0](https://github.com/rtk-ai/rtk/compare/v0.24.0...v0.25.0) (2026-03-05) - - -### Features - -* `rtk rewrite` — single source of truth for LLM hook rewrites ([#241](https://github.com/rtk-ai/rtk/issues/241)) ([f447a3d](https://github.com/rtk-ai/rtk/commit/f447a3d5b136dd5b1df3d5cc4969e29a68ba3f89)) - - -### Bug Fixes - -* **find:** accept native find flags (-name, -type, etc.) ([#211](https://github.com/rtk-ai/rtk/issues/211)) ([7ac5bc4](https://github.com/rtk-ai/rtk/commit/7ac5bc4bd3942841cc1abb53399025b4fcae10c9)) - -## [Unreleased] - -### ⚠️ Migration Required - -**Hook must be updated after upgrading** (`rtk init --global`). - -The Claude Code hook is now a thin delegator: all rewrite logic lives in the -`rtk rewrite` command (single source of truth). The old hook embedded the full -if-else mapping inline — it still works after upgrading, but won't pick up new -commands automatically. - -**Upgrade path:** -```bash -cargo install rtk # upgrade binary -rtk init --global # replace old hook with thin delegator -``` - -Running `rtk init` without `--global` updates the project-level hook only. -Users who skip this step keep the old hook working as before — no immediate -breakage, but future rule additions won't take effect until they migrate. - -### Features - -* **rewrite**: add `rtk rewrite` command — single source of truth for hook rewrites ([#241](https://github.com/rtk-ai/rtk/pull/241)) - - New `src/discover/registry.rs` handles all command → RTK mapping - - Hook reduced to ~50 lines (thin delegator), no duplicate logic - - New commands automatically available in hook without hook file changes - - Supports compound commands (`&&`, `||`, `;`, `|`, `&`) and env prefixes -* **discover**: extract rules/patterns into `src/discover/rules.rs` — adding a command now means editing one file only -* **fix**: add `aws` and `psql` to rewrite registry (were missing despite modules existing since 0.24.0) - -### Tests - -* +48 regression tests covering all command categories: aws, psql, Python, Go, JS/TS, - compound operators, sudo/env prefixes, registry invariants (607 total, was 559) -* +5 tests for uninstall `--claude-md` artifact cleanup (614 total) - -## [0.24.0](https://github.com/rtk-ai/rtk/compare/v0.23.0...v0.24.0) (2026-03-04) - - -### Features - -* add AWS CLI and psql modules with token-optimized output ([#216](https://github.com/rtk-ai/rtk/issues/216)) ([b934466](https://github.com/rtk-ai/rtk/commit/b934466364c131de2656eefabe933965f8424e18)) -* passthrough fallback when Clap parse fails + review fixes ([#200](https://github.com/rtk-ai/rtk/issues/200)) ([772b501](https://github.com/rtk-ai/rtk/commit/772b5012ede833c3f156816f212d469560449a30)) -* **security:** add SHA-256 hook integrity verification ([f2caca3](https://github.com/rtk-ai/rtk/commit/f2caca3abc330fb45a466af6a837ed79c3b00b40)) - - -### Bug Fixes - -* **git:** propagate exit codes in push/pull/fetch/stash/worktree ([#234](https://github.com/rtk-ai/rtk/issues/234)) ([5cfaecc](https://github.com/rtk-ai/rtk/commit/5cfaeccaba2fc6e1fe5284f57b7af7ec7c0a224d)) -* **playwright:** fix JSON parser to match real Playwright output format ([#193](https://github.com/rtk-ai/rtk/issues/193)) ([4eb6cf4](https://github.com/rtk-ai/rtk/commit/4eb6cf4b1a2333cb710970e40a96f1004d4ab0fa)) -* support additional git global options (--no-pager, --no-optional-locks, --bare, --literal-pathspecs) ([68ca712](https://github.com/rtk-ai/rtk/commit/68ca7126d45609a41dbff95e2770d58a11ebc0a3)) -* support git global options (-C, -c, --git-dir, --work-tree, --no-pager, --no-optional-locks, --bare, --literal-pathspecs) ([a6ccefe](https://github.com/rtk-ai/rtk/commit/a6ccefe8e71372b61e6e556f0d36a944d1bcbd70)) -* support git global options (-C, -c, --git-dir, --work-tree) ([982084e](https://github.com/rtk-ai/rtk/commit/982084ee34c17d2fe89ff9f4839374bf0caa2d19)) -* update version refs to 0.23.0, module count to 51, fmt upstream files ([eed0188](https://github.com/rtk-ai/rtk/commit/eed018814b141ada8140f350adc26d9f104cf368)) - -## [0.23.0](https://github.com/rtk-ai/rtk/compare/v0.22.2...v0.23.0) (2026-02-28) - - -### Features - -* add mypy command with grouped error output ([#109](https://github.com/rtk-ai/rtk/issues/109)) ([e8ef341](https://github.com/rtk-ai/rtk/commit/e8ef3418537247043808dc3c88bfd189b717a0a1)) -* **gain:** add per-project token savings with -p flag ([#128](https://github.com/rtk-ai/rtk/issues/128)) ([2b550ee](https://github.com/rtk-ai/rtk/commit/2b550eebd6219a4844488d8fde1842ba3c6dec25)) - - -### Bug Fixes - -* eliminate duplicate output when grep-ing function names from git show ([#248](https://github.com/rtk-ai/rtk/issues/248)) ([a6f65f1](https://github.com/rtk-ai/rtk/commit/a6f65f11da71936d148a2562216ab45b4c4b04a0)) -* filter docker compose hook rewrites to supported subcommands ([#245](https://github.com/rtk-ai/rtk/issues/245)) ([dbbf980](https://github.com/rtk-ai/rtk/commit/dbbf980f3ba9a51d0f7eb703e7b3c52fde2b784f)), closes [#244](https://github.com/rtk-ai/rtk/issues/244) -* **registry:** "fi" in IGNORED_PREFIXES shadows find commands ([#246](https://github.com/rtk-ai/rtk/issues/246)) ([48965c8](https://github.com/rtk-ai/rtk/commit/48965c85d2dd274bbdcf27b11850ccd38909e6f4)) -* remove personal preferences from project CLAUDE.md ([3a8044e](https://github.com/rtk-ai/rtk/commit/3a8044ef6991b2208d904b7401975fcfcb165cdb)) -* remove personal preferences from project CLAUDE.md ([d362ad0](https://github.com/rtk-ai/rtk/commit/d362ad0e4968cfc6aa93f9ef163512a692ca5d1b)) -* remove remaining personal project reference from CLAUDE.md ([5b59700](https://github.com/rtk-ai/rtk/commit/5b597002dcd99029cb9c0da9b6d38b44021bdb3a)) -* remove remaining personal project reference from CLAUDE.md ([dc09265](https://github.com/rtk-ai/rtk/commit/dc092655fb84a7c19a477e731eed87df5ad0b89f)) -* surface build failures in go test summary ([#274](https://github.com/rtk-ai/rtk/issues/274)) ([b405e48](https://github.com/rtk-ai/rtk/commit/b405e48ca6c4be3ba702a5d9092fa4da4dff51dc)) - -## [0.22.2](https://github.com/rtk-ai/rtk/compare/v0.22.1...v0.22.2) (2026-02-20) - - -### Bug Fixes - -* **grep:** accept -n flag for grep/rg compatibility ([7d561cc](https://github.com/rtk-ai/rtk/commit/7d561cca51e4e177d353e6514a618e5bb09eebc6)) -* **playwright:** fix JSON parser and binary resolution ([#215](https://github.com/rtk-ai/rtk/issues/215)) ([461856c](https://github.com/rtk-ai/rtk/commit/461856c8fd78cce8e2d875ae878111d7cb3610cd)) -* propagate rg exit code in rtk grep for CLI parity ([#227](https://github.com/rtk-ai/rtk/issues/227)) ([f1be885](https://github.com/rtk-ai/rtk/commit/f1be88565e602d3b6777f629d417e957a62daae2)), closes [#162](https://github.com/rtk-ai/rtk/issues/162) - -## [0.22.1](https://github.com/rtk-ai/rtk/compare/v0.22.0...v0.22.1) (2026-02-19) - - -### Bug Fixes - -* git branch creation silently swallowed by list mode ([#194](https://github.com/rtk-ai/rtk/issues/194)) ([88dc752](https://github.com/rtk-ai/rtk/commit/88dc752220dc79dfa09b871065b28ae6ef907231)) -* **git:** support multiple -m flags in git commit ([292225f](https://github.com/rtk-ai/rtk/commit/292225f2dd09bfc5274cc8b4ed92d1a519929629)) -* **git:** support multiple -m flags in git commit ([c18553a](https://github.com/rtk-ai/rtk/commit/c18553a55c1192610525a5341a183da46c59d50c)) -* **grep:** translate BRE \| alternation and strip -r flag for rg ([#206](https://github.com/rtk-ai/rtk/issues/206)) ([70d1b04](https://github.com/rtk-ai/rtk/commit/70d1b04093a3dfcc99991502f1530cbb13bae872)) -* propagate linter exit code in rtk lint ([#207](https://github.com/rtk-ai/rtk/issues/207)) ([8e826fc](https://github.com/rtk-ai/rtk/commit/8e826fc89fe7350df82ee2b1bae8104da609f2b2)), closes [#185](https://github.com/rtk-ai/rtk/issues/185) -* smart markdown body filter for gh issue/pr view ([#188](https://github.com/rtk-ai/rtk/issues/188)) ([#214](https://github.com/rtk-ai/rtk/issues/214)) ([4208015](https://github.com/rtk-ai/rtk/commit/4208015cce757654c150f3d71ddd004d22b4dd25)) - -## [0.22.0](https://github.com/rtk-ai/rtk/compare/v0.21.1...v0.22.0) (2026-02-18) - - -### Features - -* add `rtk wc` command for compact word/line/byte counts ([#175](https://github.com/rtk-ai/rtk/issues/175)) ([393fa5b](https://github.com/rtk-ai/rtk/commit/393fa5ba2bda0eb1f8655a34084ea4c1e08070ae)) - -## [0.21.1](https://github.com/rtk-ai/rtk/compare/v0.21.0...v0.21.1) (2026-02-17) - - -### Bug Fixes - -* gh run view drops --log-failed, --log, --json flags ([#159](https://github.com/rtk-ai/rtk/issues/159)) ([d196c2d](https://github.com/rtk-ai/rtk/commit/d196c2d2df9b7a807e02ace557a4eea45cfee77d)) - -## [0.21.0](https://github.com/rtk-ai/rtk/compare/v0.20.1...v0.21.0) (2026-02-17) - - -### Features - -* **docker:** add docker compose support ([#110](https://github.com/rtk-ai/rtk/issues/110)) ([510c491](https://github.com/rtk-ai/rtk/commit/510c491238731b71b58923a0f20443ade6df5ae7)) - -## [0.20.1](https://github.com/rtk-ai/rtk/compare/v0.20.0...v0.20.1) (2026-02-17) - - -### Bug Fixes - -* install to ~/.local/bin instead of /usr/local/bin (closes [#155](https://github.com/rtk-ai/rtk/issues/155)) ([#161](https://github.com/rtk-ai/rtk/issues/161)) ([0b34772](https://github.com/rtk-ai/rtk/commit/0b34772a679f3c6b5dd9609af2f6eec6d79e4a64)) - -## [0.20.0](https://github.com/rtk-ai/rtk/compare/v0.19.0...v0.20.0) (2026-02-16) - - -### Features - -* add hook audit mode for verifiable rewrite metrics ([#151](https://github.com/rtk-ai/rtk/issues/151)) ([70c3786](https://github.com/rtk-ai/rtk/commit/70c37867e7282ee0ccf200022ecef8c6e4ab52f4)) - -## [0.19.0](https://github.com/rtk-ai/rtk/compare/v0.18.1...v0.19.0) (2026-02-16) - - -### Features - -* tee raw output to file for LLM re-read without re-run ([#134](https://github.com/rtk-ai/rtk/issues/134)) ([a08a62b](https://github.com/rtk-ai/rtk/commit/a08a62b4e3b3c6a2ad933978b1143dcfc45cf891)) - -## [0.18.1](https://github.com/rtk-ai/rtk/compare/v0.18.0...v0.18.1) (2026-02-15) - - -### Bug Fixes - -* update ARCHITECTURE.md version to 0.18.0 ([398cb08](https://github.com/rtk-ai/rtk/commit/398cb08125410a4de11162720cf3499d3c76f12d)) -* update version references to 0.16.0 in README.md and CLAUDE.md ([ec54833](https://github.com/rtk-ai/rtk/commit/ec54833621c8ca666735e1a08ed5583624b250c1)) -* update version references to 0.18.0 in docs ([c73ed47](https://github.com/rtk-ai/rtk/commit/c73ed470a79ab9e4771d2ad65394859e672b4123)) - -## [0.18.0](https://github.com/rtk-ai/rtk/compare/v0.17.0...v0.18.0) (2026-02-15) - - -### Features - -* **gain:** colored dashboard with efficiency meter and impact bars ([#129](https://github.com/rtk-ai/rtk/issues/129)) ([606b86e](https://github.com/rtk-ai/rtk/commit/606b86ed43902dc894e6f1711f6fe7debedc2530)) - -## [0.17.0](https://github.com/rtk-ai/rtk/compare/v0.16.0...v0.17.0) (2026-02-15) - - -### Features - -* **cargo:** add cargo nextest support with failures-only output ([#107](https://github.com/rtk-ai/rtk/issues/107)) ([68fd570](https://github.com/rtk-ai/rtk/commit/68fd570f2b7d5aaae7b37b07eb24eae21542595e)) -* **hook:** handle global options before subcommands ([#99](https://github.com/rtk-ai/rtk/issues/99)) ([7401f10](https://github.com/rtk-ai/rtk/commit/7401f1099f3ef14598f11947262756e3f19fce8f)) - -## [0.16.0](https://github.com/rtk-ai/rtk/compare/v0.15.4...v0.16.0) (2026-02-14) - - -### Features - -* **python:** add lint dispatcher + universal format command ([#100](https://github.com/rtk-ai/rtk/issues/100)) ([4cae6b6](https://github.com/rtk-ai/rtk/commit/4cae6b6c9a4fbc91c56a99f640d217478b92e6d9)) - -## [0.15.4](https://github.com/rtk-ai/rtk/compare/v0.15.3...v0.15.4) (2026-02-14) - - -### Bug Fixes - -* **git:** fix for issue [#82](https://github.com/rtk-ai/rtk/issues/82) ([04e6bb0](https://github.com/rtk-ai/rtk/commit/04e6bb032ccd67b51fb69e326e27eff66c934043)) -* **git:** Returns "Not a git repository" when git status is executed in a non-repo folder [#82](https://github.com/rtk-ai/rtk/issues/82) ([d4cb2c0](https://github.com/rtk-ai/rtk/commit/d4cb2c08100d04755fa776ec8000c0b9673e4370)) - -## [0.15.3](https://github.com/rtk-ai/rtk/compare/v0.15.2...v0.15.3) (2026-02-13) - - -### Bug Fixes - -* prevent UTF-8 panics on multi-byte characters ([#93](https://github.com/rtk-ai/rtk/issues/93)) ([155e264](https://github.com/rtk-ai/rtk/commit/155e26423d1fe2acbaed3dc1aab8c365324d53e0)) - -## [0.15.2](https://github.com/rtk-ai/rtk/compare/v0.15.1...v0.15.2) (2026-02-13) - - -### Bug Fixes - -* **hook:** use POSIX character classes for cross-platform grep compatibility ([#98](https://github.com/rtk-ai/rtk/issues/98)) ([4aafc83](https://github.com/rtk-ai/rtk/commit/4aafc832d4bdd438609358e2737a96bee4bb2467)) - -## [0.15.1](https://github.com/rtk-ai/rtk/compare/v0.15.0...v0.15.1) (2026-02-12) - - -### Bug Fixes - -* improve CI reliability and hook coverage ([#95](https://github.com/rtk-ai/rtk/issues/95)) ([ac80bfa](https://github.com/rtk-ai/rtk/commit/ac80bfa88f91dfaf562cdd786ecd3048c554e4f7)) -* **vitest:** robust JSON extraction for pnpm/dotenv prefixes ([#92](https://github.com/rtk-ai/rtk/issues/92)) ([e5adba8](https://github.com/rtk-ai/rtk/commit/e5adba8b214a6609cf1a2cda05f21bcf2a1adb94)) - -## [0.15.0](https://github.com/rtk-ai/rtk/compare/v0.14.0...v0.15.0) (2026-02-12) - - -### Features - -* add Python and Go support ([#88](https://github.com/rtk-ai/rtk/issues/88)) ([a005bb1](https://github.com/rtk-ai/rtk/commit/a005bb15c030e16b7b87062317bddf50e12c6f32)) -* **cargo:** aggregate test output into single line ([#83](https://github.com/rtk-ai/rtk/issues/83)) ([#85](https://github.com/rtk-ai/rtk/issues/85)) ([06b1049](https://github.com/rtk-ai/rtk/commit/06b10491f926f9eca4323c80d00530a1598ec649)) -* make install-local.sh self-contained ([#89](https://github.com/rtk-ai/rtk/issues/89)) ([b82ad16](https://github.com/rtk-ai/rtk/commit/b82ad168533881757f45e28826cb0c4bd4cc6f97)) - -## [0.14.0](https://github.com/rtk-ai/rtk/compare/v0.13.1...v0.14.0) (2026-02-12) - - -### Features - -* **ci:** automate Homebrew formula update on release ([#80](https://github.com/rtk-ai/rtk/issues/80)) ([a0d2184](https://github.com/rtk-ai/rtk/commit/a0d2184bfef4d0a05225df5a83eedba3c35865b3)) - - -### Bug Fixes - -* add website URL (rtk-ai.app) across project metadata ([#81](https://github.com/rtk-ai/rtk/issues/81)) ([c84fa3c](https://github.com/rtk-ai/rtk/commit/c84fa3c060c7acccaedb617852938c894f30f81e)) -* update stale repo URLs from pszymkowiak/rtk to rtk-ai/rtk ([#78](https://github.com/rtk-ai/rtk/issues/78)) ([55d010a](https://github.com/rtk-ai/rtk/commit/55d010ad5eced14f525e659f9f35d051644a1246)) - -## [0.13.1](https://github.com/rtk-ai/rtk/compare/v0.13.0...v0.13.1) (2026-02-12) - - -### Bug Fixes - -* **ci:** fix release artifacts not uploading ([#73](https://github.com/rtk-ai/rtk/issues/73)) ([bb20b1e](https://github.com/rtk-ai/rtk/commit/bb20b1e9e1619e0d824eb0e0b87109f30bf4f513)) -* **ci:** fix release workflow not uploading artifacts to GitHub releases ([bd76b36](https://github.com/rtk-ai/rtk/commit/bd76b361908d10cce508aff6ac443340dcfbdd76)) - -## [0.13.0](https://github.com/rtk-ai/rtk/compare/v0.12.0...v0.13.0) (2026-02-12) - - -### Features - -* **sqlite:** add custom sqlite db location ([6e181ae](https://github.com/rtk-ai/rtk/commit/6e181aec087edb50625e08b72fe7abdadbb6c72b)) -* **sqlite:** add custom sqlite db location ([93364b5](https://github.com/rtk-ai/rtk/commit/93364b5457619201c656fc2423763fea77633f15)) - -## [0.12.0](https://github.com/rtk-ai/rtk/compare/v0.11.0...v0.12.0) (2026-02-09) - - -### Features - -* **cargo:** add `cargo install` filtering with 80-90% token reduction ([645a773](https://github.com/rtk-ai/rtk/commit/645a773a65bb57dc2635aa405a6e2b87534491e3)), closes [#69](https://github.com/rtk-ai/rtk/issues/69) -* **cargo:** add cargo install filtering ([447002f](https://github.com/rtk-ai/rtk/commit/447002f8ba3bbd2b398f85db19b50982df817a02)) - -## [0.11.0](https://github.com/rtk-ai/rtk/compare/v0.10.0...v0.11.0) (2026-02-07) - - -### Features - -* **init:** auto-patch settings.json for frictionless hook installation ([2db7197](https://github.com/rtk-ai/rtk/commit/2db7197e020857c02857c8ef836279c3fd660baf)) - -## [Unreleased] - -### Added -- **settings.json auto-patch** for frictionless hook installation - - Default `rtk init -g` now prompts to patch settings.json [y/N] - - `--auto-patch`: Patch immediately without prompting (CI/CD workflows) - - `--no-patch`: Skip patching, print manual instructions instead - - Automatic backup: creates `settings.json.bak` before modification - - Idempotent: detects existing hook, skips modification if present - - `rtk init --show` now displays settings.json status -- **Uninstall command** for complete RTK removal - - `rtk init -g --uninstall` removes hook, RTK.md, CLAUDE.md reference, and settings.json entry - - Restores clean state for fresh installation or testing -- **Improved error handling** with detailed context messages - - All error messages now include file paths and actionable hints - - UTF-8 validation for hook paths - - Disk space hints on write failures - -### Changed -- Refactored `insert_hook_entry()` to use idiomatic Rust `entry()` API -- Simplified `hook_already_present()` logic with iterator chains -- Improved atomic write error messages for better debugging -## [0.10.0](https://github.com/rtk-ai/rtk/compare/v0.9.4...v0.10.0) (2026-02-07) - - -### Features - -* Hook-first installation with 99.5% token reduction ([e7f80ad](https://github.com/rtk-ai/rtk/commit/e7f80ad29481393d16d19f55b3c2171a4b8b7915)) -* **init:** refactor to hook-first with slim RTK.md ([9620f66](https://github.com/rtk-ai/rtk/commit/9620f66cd64c299426958d4d3d65bd8d1a9bc92d)) - -## [0.9.4](https://github.com/rtk-ai/rtk/compare/v0.9.3...v0.9.4) (2026-02-06) - - -### Bug Fixes - -* **discover:** add cargo check support, wire RtkStatus::Passthrough, enhance rtk init ([d5f8a94](https://github.com/rtk-ai/rtk/commit/d5f8a9460421821861a32eedefc0800fb7720912)) - -## [0.9.3](https://github.com/rtk-ai/rtk/compare/v0.9.2...v0.9.3) (2026-02-06) - - -### Bug Fixes - -* P0 crashes + cargo check + dedup utilities + discover status ([05078ff](https://github.com/rtk-ai/rtk/commit/05078ff2dab0c8745b9fb44b1d462c0d32ae8d77)) -* P0 crashes + cargo check + dedup utilities + discover status ([60d2d25](https://github.com/rtk-ai/rtk/commit/60d2d252efbedaebae750b3122385b2377ab01eb)) - -## [0.9.2](https://github.com/rtk-ai/rtk/compare/v0.9.1...v0.9.2) (2026-02-05) - - -### Bug Fixes - -* **git:** accept native git flags in add command (including -A) ([2ade8fe](https://github.com/rtk-ai/rtk/commit/2ade8fe030d8b1bc2fa294aa710ed1f5f877136f)) -* **git:** accept native git flags in add command (including -A) ([40e7ead](https://github.com/rtk-ai/rtk/commit/40e7eadbaf0b89a54b63bea73014eac7cf9afb05)) - -## [0.9.1](https://github.com/rtk-ai/rtk/compare/v0.9.0...v0.9.1) (2026-02-04) - - -### Bug Fixes - -* **tsc:** show every TypeScript error instead of collapsing by code ([3df8ce5](https://github.com/rtk-ai/rtk/commit/3df8ce552585d8d0a36f9c938d381ac0bc07b220)) -* **tsc:** show every TypeScript error instead of collapsing by code ([67e8de8](https://github.com/rtk-ai/rtk/commit/67e8de8732363d111583e5b514d05e092355b97e)) - -## [0.9.0](https://github.com/rtk-ai/rtk/compare/v0.8.1...v0.9.0) (2026-02-03) - - -### Features - -* add rtk tree + fix rtk ls + audit phase 1-2 ([278cc57](https://github.com/rtk-ai/rtk/commit/278cc5700bc39770841d157f9c53161f8d62df1e)) -* audit phase 3 + tracking validation + rtk learn ([7975624](https://github.com/rtk-ai/rtk/commit/7975624d0a83c44dfeb073e17fd07dbc62dc8329)) -* **git:** add fallback passthrough for unsupported subcommands ([32bbd02](https://github.com/rtk-ai/rtk/commit/32bbd025345872e46f67e8c999ecc6f71891856b)) -* **grep:** add extra args passthrough (-i, -A/-B/-C, etc.) ([a240d1a](https://github.com/rtk-ai/rtk/commit/a240d1a1ee0d94c178d0c54b411eded6c7839599)) -* **pnpm:** add fallback passthrough for unsupported subcommands ([614ff5c](https://github.com/rtk-ai/rtk/commit/614ff5c13f526f537231aaa9fa098763822b4ee0)) -* **read:** add stdin support via "-" path ([060c38b](https://github.com/rtk-ai/rtk/commit/060c38b3c1ab29070c16c584ea29da3d5ca28f3d)) -* rtk tree + fix rtk ls + full audit (phase 1-2-3) ([cb83da1](https://github.com/rtk-ai/rtk/commit/cb83da104f7beba3035225858d7f6eb2979d950c)) - - -### Bug Fixes - -* **docs:** escape HTML tags in rustdoc comments ([b13d92c](https://github.com/rtk-ai/rtk/commit/b13d92c9ea83e28e97847e0a6da696053364bbfc)) -* **find:** rewrite with ignore crate + fix json stdin + benchmark pipeline ([fcc1462](https://github.com/rtk-ai/rtk/commit/fcc14624f89a7aa9742de4e7bc7b126d6d030871)) -* **ls:** compact output (-72% tokens) + fix discover panic ([ea7cdb7](https://github.com/rtk-ai/rtk/commit/ea7cdb7a3b622f62e0a085144a637a22108ffdb7)) - -## [0.8.1](https://github.com/rtk-ai/rtk/compare/v0.8.0...v0.8.1) (2026-02-02) - - -### Bug Fixes - -* allow git status to accept native flags ([a7ea143](https://github.com/rtk-ai/rtk/commit/a7ea1439fb99a9bd02292068625bed6237f6be0c)) -* allow git status to accept native flags ([a27bce8](https://github.com/rtk-ai/rtk/commit/a27bce82f09701cb9df2ed958f682ab5ac8f954e)) - -## [0.8.0](https://github.com/rtk-ai/rtk/compare/v0.7.1...v0.8.0) (2026-02-02) - - -### Features - -* add comprehensive security review workflow for PRs ([1ca6e81](https://github.com/rtk-ai/rtk/commit/1ca6e81bdf16a7eab503d52b342846c3519d89ff)) -* add comprehensive security review workflow for PRs ([66101eb](https://github.com/rtk-ai/rtk/commit/66101ebb65076359a1530d8f19e11a17c268bce2)) - -## [0.7.1](https://github.com/pszymkowiak/rtk/compare/v0.7.0...v0.7.1) (2026-02-02) - - -### Features - -* **execution time tracking**: Add command execution time metrics to `rtk gain` analytics - - Total execution time and average time per command displayed in summary - - Time column in "By Command" breakdown showing average execution duration - - Daily breakdown (`--daily`) includes time metrics per day - - JSON export includes `total_time_ms` and `avg_time_ms` fields - - CSV export includes execution time columns - - Backward compatible: historical data shows 0ms (pre-tracking) - - Negligible overhead: <0.1ms per command - - New SQLite column: `exec_time_ms` in commands table -* **parser infrastructure**: Three-tier fallback system for robust output parsing - - Tier 1: Full JSON parsing with complete structured data - - Tier 2: Degraded parsing with regex fallback and warnings - - Tier 3: Passthrough with truncated raw output and error markers - - Guarantees RTK never returns false data silently -* **migrate commands to OutputParser**: vitest, playwright, pnpm now use robust parsing - - JSON parsing with safe fallbacks for all modern JS tooling - - Improved error handling and debugging visibility -* **local LLM analysis**: Add economics analysis and comprehensive test scripts - - `scripts/rtk-economics.sh` for token savings ROI analysis - - `scripts/test-all.sh` with 69 assertions covering all commands - - `scripts/test-aristote.sh` for T3 Stack project validation - - -### Bug Fixes - -* convert rtk ls from reimplementation to native proxy for better reliability -* trigger release build after release-please creates tag - - -### Documentation - -* add execution time tracking test guide (TEST_EXEC_TIME.md) -* comprehensive parser infrastructure documentation (src/parser/README.md) - -## [0.7.0](https://github.com/pszymkowiak/rtk/compare/v0.6.0...v0.7.0) (2026-02-01) - - -### Features - -* add discover command, auto-rewrite hook, and git show support ([ff1c759](https://github.com/pszymkowiak/rtk/commit/ff1c7598c240ca69ab51f507fe45d99d339152a0)) -* discover command, auto-rewrite hook, git show ([c9c64cf](https://github.com/pszymkowiak/rtk/commit/c9c64cfd30e2c867ce1df4be508415635d20132d)) - - -### Bug Fixes - -* forward args in rtk git push/pull to support -u, remote, branch ([4bb0130](https://github.com/pszymkowiak/rtk/commit/4bb0130695ad2f5d91123afac2e3303e510b240c)) - -## [0.6.0](https://github.com/pszymkowiak/rtk/compare/v0.5.2...v0.6.0) (2026-02-01) - - -### Features - -* cargo build/test/clippy with compact output ([bfd5646](https://github.com/pszymkowiak/rtk/commit/bfd5646f4eac32b46dbec05f923352a3e50c19ef)) -* curl with auto-JSON detection ([314accb](https://github.com/pszymkowiak/rtk/commit/314accbfd9ac82cc050155c6c47dfb76acab14ce)) -* gh pr create/merge/diff/comment/edit + gh api ([517a93d](https://github.com/pszymkowiak/rtk/commit/517a93d0e4497414efe7486410c72afdad5f8a26)) -* git branch, fetch, stash, worktree commands ([bc31da8](https://github.com/pszymkowiak/rtk/commit/bc31da8ad9d9e91eee8af8020e5bd7008da95dd2)) -* npm/npx routing, pnpm build/typecheck, --skip-env flag ([49b3cf2](https://github.com/pszymkowiak/rtk/commit/49b3cf293d856ff3001c46cff8fee9de9ef501c5)) -* shared infrastructure for new commands ([6c60888](https://github.com/pszymkowiak/rtk/commit/6c608880e9ecbb2b3569f875e7fad37d1184d751)) -* shared infrastructure for new commands ([9dbc117](https://github.com/pszymkowiak/rtk/commit/9dbc1178e7f7fab8a0695b624ed3744ab1a8bf02)) - -## [0.5.2](https://github.com/pszymkowiak/rtk/compare/v0.5.1...v0.5.2) (2026-01-30) - - -### Bug Fixes - -* release pipeline trigger and version-agnostic package URLs ([108d0b5](https://github.com/pszymkowiak/rtk/commit/108d0b5ea316ab33c6998fb57b2caf8c65ebe3ef)) -* release pipeline trigger and version-agnostic package URLs ([264539c](https://github.com/pszymkowiak/rtk/commit/264539cf20a29de0d9a1a39029c04cb8eb1b8f10)) - -## [0.5.1](https://github.com/pszymkowiak/rtk/compare/v0.5.0...v0.5.1) (2026-01-30) - - -### Bug Fixes - -* 3 issues (latest tag, ccusage fallback, versioning) ([d773ec3](https://github.com/pszymkowiak/rtk/commit/d773ec3ea515441e6c62bbac829f45660cfaccde)) -* patrick's 3 issues (latest tag, ccusage fallback, versioning) ([9e322e2](https://github.com/pszymkowiak/rtk/commit/9e322e2aee9f7239cf04ce1bf9971920035ac4bb)) - -## [0.5.0](https://github.com/pszymkowiak/rtk/compare/v0.4.0...v0.5.0) (2026-01-30) - - -### Features - -* add comprehensive claude code economics analysis ([ec1cf9a](https://github.com/pszymkowiak/rtk/commit/ec1cf9a56dd52565516823f55f99a205cfc04558)) -* comprehensive economics analysis and code quality improvements ([8e72e7a](https://github.com/pszymkowiak/rtk/commit/8e72e7a8b8ac7e94e9b13958d8b6b8e9bf630660)) - - -### Bug Fixes - -* comprehensive code quality improvements ([5b840cc](https://github.com/pszymkowiak/rtk/commit/5b840cca492ea32488d8c80fd50d3802a0c41c72)) -* optimize HashMap merge and add safety checks ([3b847f8](https://github.com/pszymkowiak/rtk/commit/3b847f863a90b2e9a9b7eb570f700a376bce8b22)) - -## [0.4.0](https://github.com/pszymkowiak/rtk/compare/v0.3.1...v0.4.0) (2026-01-30) - - -### Features - -* add comprehensive temporal audit system for token savings analytics ([76703ca](https://github.com/pszymkowiak/rtk/commit/76703ca3f5d73d3345c2ed26e4de86e6df815aff)) -* Comprehensive Temporal Audit System for Token Savings Analytics ([862047e](https://github.com/pszymkowiak/rtk/commit/862047e387e95b137973983b4ebad810fe5b4431)) - -## [0.3.1](https://github.com/pszymkowiak/rtk/compare/v0.3.0...v0.3.1) (2026-01-29) - - -### Bug Fixes - -* improve command robustness and flag support ([c2cd691](https://github.com/pszymkowiak/rtk/commit/c2cd691c823c8b1dd20d50d01486664f7fd7bd28)) -* improve command robustness and flag support ([d7d8c65](https://github.com/pszymkowiak/rtk/commit/d7d8c65b86d44792e30ce3d0aff9d90af0dd49ed)) - -## [0.3.0](https://github.com/pszymkowiak/rtk/compare/v0.2.1...v0.3.0) (2026-01-29) - - -### Features - -* add --quota flag to rtk gain with tier-based analysis ([26b314d](https://github.com/pszymkowiak/rtk/commit/26b314d45b8b0a0c5c39fb0c17001ecbde9d97aa)) -* add CI/CD automation (release management and automated metrics) ([22c3017](https://github.com/pszymkowiak/rtk/commit/22c3017ed5d20e5fb6531cfd7aea5e12257e3da9)) -* add GitHub CLI integration (depends on [#9](https://github.com/pszymkowiak/rtk/issues/9)) ([341c485](https://github.com/pszymkowiak/rtk/commit/341c48520792f81889543a5dc72e572976856bbb)) -* add GitHub CLI integration with token optimizations ([0f7418e](https://github.com/pszymkowiak/rtk/commit/0f7418e958b23154cb9dcf52089a64013a666972)) -* add modern JavaScript tooling support ([b82fa85](https://github.com/pszymkowiak/rtk/commit/b82fa85ae5fe0cc1f17d8acab8c6873f436a4d62)) -* add modern JavaScript tooling support (lint, tsc, next, prettier, playwright, prisma) ([88c0174](https://github.com/pszymkowiak/rtk/commit/88c0174d32e0603f6c5dcc7f969fa8f988573ec6)) -* add Modern JS Stack commands to benchmark script ([b868987](https://github.com/pszymkowiak/rtk/commit/b868987f6f48876bb2ce9a11c9cad12725401916)) -* add quota analysis with multi-tier support ([64c0b03](https://github.com/pszymkowiak/rtk/commit/64c0b03d4e4e75a7051eac95be2d562797f1a48a)) -* add shared utils module for JS stack commands ([0fc06f9](https://github.com/pszymkowiak/rtk/commit/0fc06f95098e00addf06fe71665638ab2beb1aac)) -* CI/CD automation (versioning, benchmarks, README auto-update) ([b8bbfb8](https://github.com/pszymkowiak/rtk/commit/b8bbfb87b4dc2b664f64ee3b0231e346a2244055)) - - -### Bug Fixes - -* **ci:** correct rust-toolchain action name ([9526471](https://github.com/pszymkowiak/rtk/commit/9526471530b7d272f32aca38ace7548fd221547e)) - -## [Unreleased] - -### Added -- `prettier` command for format checking with package manager auto-detection (pnpm/yarn/npx) - - Shows only files needing formatting (~70% token reduction) - - Exit code preservation for CI/CD compatibility -- `playwright` command for E2E test output filtering (~94% token reduction) - - Shows only test failures and slow tests - - Summary with pass/fail counts and timing -- `lint` command with ESLint/Biome support and pnpm detection - - Groups violations by rule and file (~84% token reduction) - - Shows top violators for quick navigation -- `tsc` command for TypeScript compiler output filtering - - Groups errors by file and error code (~83% token reduction) - - Shows top 10 affected files -- `next` command for Next.js build/dev output filtering (87% token reduction) - - Extracts route count and bundle sizes - - Highlights warnings and oversized bundles -- `prisma` command for Prisma CLI output filtering - - Removes ASCII art and verbose logs (~88% token reduction) - - Supports generate, migrate (dev/status/deploy), and db push -- `utils` module with common utilities (truncate, strip_ansi, execute_command) - - Shared functionality for consistent output formatting - - ANSI escape code stripping for clean parsing - -### Changed -- Refactored duplicated code patterns into `utils.rs` module -- Improved package manager detection across all modern JS commands - -## [0.2.1] - 2026-01-29 - -See upstream: https://github.com/pszymkowiak/rtk - -## Links - -- **Repository**: https://github.com/rtk-ai/rtk (maintained by pszymkowiak) -- **Issues**: https://github.com/rtk-ai/rtk/issues +Track changes via Jujutsu/GitHub history. diff --git a/CLAUDE.md b/CLAUDE.md index 9e89fff38..186fbe258 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,10 +17,10 @@ This is a fork with critical fixes for git argument parsing and modern JavaScrip **Verify correct installation:** ```bash rtk --version # Should show "rtk 0.28.2" (or newer) -rtk gain # Should show token savings stats (NOT "command not found") +rtk --help # Should list RTK commands ``` -If `rtk gain` fails, you have the wrong package installed. +If `rtk --help` fails, your install is broken. ## Development Commands @@ -75,8 +75,6 @@ For the full architecture, component details, and module development patterns, s Module responsibilities are documented in each folder's `README.md` and each file's `//!` doc header. Browse `src/cmds/*/` to discover available filters. -Supported ecosystems: git/gh/gt, cargo, go/golangci-lint, npm/pnpm/npx, ruff/pytest/pip/mypy, rspec/rubocop/rake, dotnet, playwright/vitest/jest, docker/kubectl/aws. - ### Proxy Mode **Purpose**: Execute commands without filtering but track usage for metrics. @@ -85,7 +83,6 @@ Supported ecosystems: git/gh/gt, cargo, go/golangci-lint, npm/pnpm/npx, ruff/pyt **Benefits**: - **Bypass RTK filtering**: Workaround bugs or get full unfiltered output -- **Track usage metrics**: Measure which commands Claude uses most (visible in `rtk gain --history`) - **Guaranteed compatibility**: Always works even if RTK doesn't implement the command **Examples**: @@ -95,7 +92,6 @@ rtk proxy npm install express # Raw npm output (no filtering) rtk proxy curl https://api.example.com/data # Any command works ``` -All proxy commands appear in `rtk gain --history` with 0% savings (input = output). ## Coding Rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cd87369b..1e2c69f55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -251,12 +251,26 @@ For how to write tests (fixtures, snapshots, token savings verification), see [d ### Pre-Commit Gate (mandatory) -All three must pass before any PR: +This repo uses **prek** with a strict local gate. + +Install and enable hooks: + +```bash +cargo install prek +prek install --hook-type pre-commit --hook-type pre-push +``` + +Run all configured checks: ```bash -cargo fmt --all --check && cargo clippy --all-targets && cargo test +prek run --all-files ``` +The gate enforces: +- formatting/lint/tests (`cargo fmt`, `cargo check`, `cargo clippy`, `cargo test`) +- security scan at pre-push (`cargo audit`) +- architecture guards (hotspot file-size caps, dangerous pattern checks, command-module test presence, new-command wiring checks) + ### PR Testing Checklist - [ ] Unit tests added/updated for changed code diff --git a/Cargo.lock b/Cargo.lock index e65c2219f..35bbb0d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,15 +55,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -111,17 +111,11 @@ dependencies = [ "syn", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -150,9 +144,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -179,9 +173,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -189,9 +183,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -201,9 +195,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -213,15 +207,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -323,23 +317,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "equivalent" version = "1.0.2" @@ -370,9 +347,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" @@ -397,12 +374,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "percent-encoding", + "futures-core", + "futures-task", + "pin-project-lite", + "slab", ] [[package]] @@ -472,9 +464,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -515,114 +507,12 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "ignore" version = "0.4.25" @@ -641,12 +531,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -659,16 +549,18 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -687,15 +579,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "libc", ] @@ -717,12 +609,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - [[package]] name = "log" version = "0.4.29" @@ -756,9 +642,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -773,25 +659,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "prettyplease" @@ -876,20 +753,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rtk" version = "0.39.0" @@ -901,7 +764,6 @@ dependencies = [ "colored", "dirs", "flate2", - "getrandom 0.4.2", "ignore", "lazy_static", "libc", @@ -913,7 +775,6 @@ dependencies = [ "sha2", "tempfile", "toml", - "ureq", "walkdir", "which", ] @@ -945,41 +806,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -997,9 +823,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1073,21 +899,21 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "smallvec" -version = "1.15.1" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "strsim" @@ -1095,12 +921,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.117" @@ -1112,22 +932,11 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -1156,16 +965,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "toml" version = "0.8.23" @@ -1209,9 +1008,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" @@ -1225,46 +1024,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1301,11 +1060,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -1314,14 +1073,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -1332,9 +1091,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1342,9 +1101,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -1355,9 +1114,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -1396,33 +1155,13 @@ dependencies = [ "semver", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "env_home", - "rustix", - "winsafe", + "libc", ] [[package]] @@ -1502,15 +1241,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -1659,12 +1389,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1674,6 +1398,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -1753,109 +1483,20 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 30ce3d7f1..d8bb94157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,6 @@ toml = "0.8" chrono = "0.4" tempfile = "3" sha2 = "0.10" -ureq = "2" -getrandom = "0.4" flate2 = "1.0" quick-xml = "0.37" which = "8" diff --git a/DISCLAIMER.md b/DISCLAIMER.md index 398377793..825b883ea 100644 --- a/DISCLAIMER.md +++ b/DISCLAIMER.md @@ -1,29 +1,5 @@ # Disclaimer -## No Warranty +This fork is maintained independently from upstream `rtk-ai/rtk`. -This software is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. The entire risk as to the quality and performance of the software is with you. - -## Limitation of Liability - -In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. This includes, without limitation, any direct, indirect, incidental, special, exemplary, or consequential damages (including but not limited to loss of data, loss of profits, or business interruption). - -## Precompiled Binaries - -Precompiled binaries are provided solely for convenience and are covered by the same license as the source code (Apache License 2.0). They are provided without warranties or conditions of any kind. You are responsible for verifying the integrity and suitability of any binary before use. Always verify checksums when available. - -## Third-Party Dependencies - -This software incorporates third-party open-source components, each governed by their respective licenses. The authors make no representations or warranties regarding these dependencies and accept no liability for any issues arising from their use. - -## Use at Your Own Risk - -This software interacts with your development environment, file system, and external commands. It is your responsibility to ensure that its use is appropriate for your environment and complies with any applicable policies, regulations, or agreements. The authors are not responsible for any unintended side effects resulting from its use. - -## Telemetry - -This software collects anonymous, aggregate usage metrics by default and can be disabled at any time. No personally identifiable information, source code, file paths, command arguments, or secrets are collected. See the README for full details and opt-out instructions. - ---- - -See [LICENSE](LICENSE) for the full terms of the Apache License 2.0 under which this software is distributed. +Behavior, feature set, and release process may differ. diff --git a/INSTALL.md b/INSTALL.md index fb231500d..02a9d6881 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,397 +1,33 @@ -# RTK Installation Guide - For AI Coding Assistants +# Installation -## ⚠️ Name Collision Warning - -**There are TWO completely different projects named "rtk":** - -1. ✅ **Rust Token Killer** (this project) - LLM token optimizer - - Repos: `rtk-ai/rtk` - - Has `rtk gain` command for token savings stats - -2. ❌ **Rust Type Kit** (reachingforthejack/rtk) - DIFFERENT PROJECT - - Rust codebase query tool and type generator - - **DO NOT install if you want token optimization** - -## Pre-Installation Check (REQUIRED) - -**AI assistants should ALWAYS verify if RTK is already installed before attempting installation.** +## Cargo ```bash -# Check if RTK is installed -rtk --version - -# CRITICAL: Verify it's the Token Killer (not Type Kit) -rtk gain # Should show token savings stats, NOT "command not found" - -# Check installation path -which rtk +cargo install --git https://github.com/duolingo/rtk ``` -If `rtk gain` works, you have the **correct** RTK installed. **DO NOT reinstall**. Skip to "Project Initialization". - -If `rtk gain` fails but `rtk --version` succeeds, you have the **wrong** RTK (Type Kit). Uninstall and reinstall the correct one (see below). - -## Installation (only if RTK not available or wrong RTK installed) - -### Step 0: Uninstall Wrong RTK (if needed) - -If you accidentally installed Rust Type Kit: +## Homebrew ```bash -cargo uninstall rtk +brew install rtk ``` -### Quick Install (Linux/macOS) +## Verify ```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh -``` - -After installation, **verify you have the correct rtk**: -```bash -rtk gain # Must show token savings stats (not "command not found") -``` - -### Alternative: Manual Installation - -```bash -# From rtk-ai repository (NOT reachingforthejack!) -cargo install --git https://github.com/rtk-ai/rtk - -# OR (if published and correct on crates.io) -cargo install rtk - -# ALWAYS VERIFY after installation -rtk gain # MUST show token savings, not "command not found" -``` - -⚠️ **WARNING**: `cargo install rtk` from crates.io might install the wrong package. Always verify with `rtk gain`. - -## Project Initialization - -### Which mode to choose? - -``` - Do you want RTK active across ALL Claude Code projects? - │ - ├─ YES → rtk init -g (recommended) - │ Hook + RTK.md (~10 tokens in context) - │ Commands auto-rewritten transparently - │ - ├─ YES, minimal → rtk init -g --hook-only - │ Hook only, nothing added to CLAUDE.md - │ Zero tokens in context - │ - └─ NO, single project → rtk init - Local CLAUDE.md only (137 lines) - No hook, no global effect -``` - -### Recommended: Global Hook-First Setup - -**Best for: All projects, automatic RTK usage** - -```bash -rtk init -g -# → Installs hook to ~/.claude/hooks/rtk-rewrite.sh -# → Creates ~/.claude/RTK.md (10 lines, meta commands only) -# → Adds @RTK.md reference to ~/.claude/CLAUDE.md -# → Prompts: "Patch settings.json? [y/N]" -# → If yes: patches + creates backup (~/.claude/settings.json.bak) - -# Automated alternatives: -rtk init -g --auto-patch # Patch without prompting -rtk init -g --no-patch # Print manual instructions instead - -# Verify installation -rtk init --show # Check hook is installed and executable -``` - -**Token savings**: ~99.5% reduction (2000 tokens → 10 tokens in context) - -**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. - -``` - Claude Code settings.json rtk-rewrite.sh RTK binary - │ │ │ │ - │ "git status" │ │ │ - │ ──────────────────►│ │ │ - │ │ PreToolUse trigger │ │ - │ │ ───────────────────►│ │ - │ │ │ rewrite command │ - │ │ │ → rtk git status │ - │ │◄────────────────────│ │ - │ │ updated command │ │ - │ │ │ - │ execute: rtk git status │ - │ ─────────────────────────────────────────────────────────────►│ - │ │ filter - │ "3 modified, 1 untracked ✓" │ - │◄──────────────────────────────────────────────────────────────│ -``` - -**Backup Safety**: -RTK backs up existing settings.json before changes. Restore if needed: -```bash -cp ~/.claude/settings.json.bak ~/.claude/settings.json -``` - -### Alternative: Local Project Setup - -**Best for: Single project without hook** - -```bash -cd /path/to/your/project -rtk init # Creates ./CLAUDE.md with full RTK instructions (137 lines) -``` - -**Token savings**: Instructions loaded only for this project - -### Upgrading from Previous Version - -#### From old 137-line CLAUDE.md injection (pre-0.22) - -```bash -rtk init -g # Automatically migrates to hook-first mode -# → Removes old 137-line block -# → Installs hook + RTK.md -# → Adds @RTK.md reference -``` - -#### From old hook with inline logic (pre-0.24) — ⚠️ Breaking Change - -RTK 0.24.0 replaced the inline command-detection hook (~200 lines) with a **thin delegator** that calls `rtk rewrite`. The binary now contains the rewrite logic, so adding new commands no longer requires a hook update. - -The old hook still works but won't benefit from new rules added in future releases. - -```bash -# Upgrade hook to thin delegator -rtk init --global - -# Verify the new hook is active -rtk init --show -# Should show: ✅ Hook: ... (thin delegator, up to date) +rtk --version +rtk --help ``` -## Common User Flows +## Initialize hooks -### First-Time User (Recommended) ```bash -# 1. Install RTK -cargo install --git https://github.com/rtk-ai/rtk -rtk gain # Verify (must show token stats) - -# 2. Setup with prompts rtk init -g -# → Answer 'y' when prompted to patch settings.json -# → Creates backup automatically - -# 3. Restart Claude Code -# 4. Test: git status (should use rtk) -``` - -### CI/CD or Automation -```bash -# Non-interactive setup (no prompts) -rtk init -g --auto-patch - -# Verify in scripts -rtk init --show | grep "Hook:" -``` - -### Conservative User (Manual Control) -```bash -# Get manual instructions without patching -rtk init -g --no-patch - -# Review printed JSON snippet -# Manually edit ~/.claude/settings.json -# Restart Claude Code ``` -### Temporary Trial -```bash -# Install hook -rtk init -g --auto-patch - -# Later: remove everything -rtk init -g --uninstall - -# Restore backup if needed -cp ~/.claude/settings.json.bak ~/.claude/settings.json -``` - -## Installation Verification +## Smoke test ```bash -# Basic test -rtk ls . - -# Test with git rtk git status - -# Test with pnpm -rtk pnpm list - -# Test with Vitest -rtk vitest -``` - -## Uninstalling - -### Complete Removal (Global Installations Only) - -```bash -# Complete removal (global installations only) -rtk init -g --uninstall - -# What gets removed: -# - Hook: ~/.claude/hooks/rtk-rewrite.sh -# - Context: ~/.claude/RTK.md -# - Reference: @RTK.md line from ~/.claude/CLAUDE.md -# - Registration: RTK hook entry from settings.json - -# Restart Claude Code after uninstall -``` - -**For Local Projects**: Manually remove RTK block from `./CLAUDE.md` - -### Binary Removal - -```bash -# If installed via cargo -cargo uninstall rtk - -# If installed via package manager -brew uninstall rtk # macOS Homebrew -sudo apt remove rtk # Debian/Ubuntu -sudo dnf remove rtk # Fedora/RHEL -``` - -### Restore from Backup (if needed) - -```bash -cp ~/.claude/settings.json.bak ~/.claude/settings.json -``` - -## Essential Commands - -### Files -```bash -rtk ls . # Compact tree view -rtk read file.rs # Optimized reading -rtk grep "pattern" . # Grouped search results -``` - -### Git -```bash -rtk git status # Compact status -rtk git log -n 10 # Condensed logs -rtk git diff # Optimized diff -rtk git add . # → "ok ✓" -rtk git commit -m "msg" # → "ok ✓ abc1234" -rtk git push # → "ok ✓ main" +rtk ls ``` - -### Pnpm (fork only) -```bash -rtk pnpm list # Dependency tree (-70% tokens) -rtk pnpm outdated # Available updates (-80-90%) -rtk pnpm install # Silent installation -``` - -### Tests -```bash -rtk cargo test # Filtered Cargo test output (-90%) -rtk go test # Filtered Go tests (NDJSON, -90%) -rtk jest # Filtered Jest output (-99.6%) -rtk vitest # Filtered Vitest output (-99.6%) -rtk playwright test # Filtered Playwright output (-94%) -rtk pytest # Filtered Python tests (-90%) -rtk rake test # Filtered Ruby tests (-90%) -rtk rspec # Filtered RSpec tests (-60%) -rtk test # Generic test wrapper - failures only (-90%) -``` - -### Statistics -```bash -rtk gain # Token savings -rtk gain --graph # With ASCII graph -rtk gain --history # With command history -``` - -## Validated Token Savings - -### Production T3 Stack Project -| Operation | Standard | RTK | Reduction | -|-----------|----------|-----|-----------| -| `vitest` | 102,199 chars | 377 chars | **-99.6%** | -| `git status` | 529 chars | 217 chars | **-59%** | -| `pnpm list` | ~8,000 tokens | ~2,400 | **-70%** | -| `pnpm outdated` | ~12,000 tokens | ~1,200-2,400 | **-80-90%** | - -### Typical Claude Code Session (30 min) -- **Without RTK**: ~150,000 tokens -- **With RTK**: ~45,000 tokens -- **Savings**: **70% reduction** - -## Troubleshooting - -### RTK command not found after installation -```bash -# Check PATH -echo $PATH | grep -o '[^:]*\.cargo[^:]*' - -# Add to PATH if needed (~/.bashrc or ~/.zshrc) -export PATH="$HOME/.cargo/bin:$PATH" - -# Reload shell -source ~/.bashrc # or source ~/.zshrc -``` - -### RTK command not available (e.g., vitest) -```bash -# Check branch -cd /path/to/rtk -git branch - -# Switch to feat/vitest-support if needed -git checkout feat/vitest-support - -# Reinstall -cargo install --path . --force -``` - -### Compilation error -```bash -# Update Rust -rustup update stable - -# Clean and recompile -cargo clean -cargo build --release -cargo install --path . --force -``` - -## Support and Contributing - -- **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 - -⚠️ **If you installed the wrong rtk (Type Kit)**, see [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md#problem-rtk-gain-command-not-found) - -## AI Assistant Checklist - -Before each session: - -- [ ] Verify RTK is installed: `rtk --version` -- [ ] If not installed → follow "Install from fork" -- [ ] If project not initialized → `rtk init` -- [ ] Use `rtk` for ALL git/pnpm/test/vitest commands -- [ ] Check savings: `rtk gain` - -**Golden Rule**: AI coding assistants should ALWAYS use `rtk` as a proxy for shell commands that generate verbose output (git, pnpm, npm, cargo test, vitest, docker, kubectl). diff --git a/README.md b/README.md index 1452b1ca8..a15330b21 100644 --- a/README.md +++ b/README.md @@ -1,488 +1,55 @@ -

- RTK - Rust Token Killer -

+# rtk -

- High-performance CLI proxy that reduces LLM token consumption by 60-90% -

+`rtk` is a Rust CLI proxy that filters noisy command output before it reaches an LLM. -

- CI - Release - License: MIT - Discord - Homebrew -

+## Scope of this fork -

- Website • - Install • - Troubleshooting • - Architecture • - Discord -

+This fork keeps output filtering and hook integrations, and removes: -

- English • - Francais • - 中文 • - 日本語 • - 한국어 • - Espanol -

+- OpenClaw integration +- outbound telemetry +- outbound telemetry/call-home behavior; local `rtk gain` stats remain local-only ---- - -rtk filters and compresses command outputs before they reach your LLM context. Single Rust binary, 100+ supported commands, <10ms overhead. - -## Token Savings (30-min Claude Code Session) - -| Operation | Frequency | Standard | rtk | Savings | -|-----------|-----------|----------|-----|---------| -| `ls` / `tree` | 10x | 2,000 | 400 | -80% | -| `cat` / `read` | 20x | 40,000 | 12,000 | -70% | -| `grep` / `rg` | 8x | 16,000 | 3,200 | -80% | -| `git status` | 10x | 3,000 | 600 | -80% | -| `git diff` | 5x | 10,000 | 2,500 | -75% | -| `git log` | 5x | 2,500 | 500 | -80% | -| `git add/commit/push` | 8x | 1,600 | 120 | -92% | -| `cargo test` / `npm test` | 5x | 25,000 | 2,500 | -90% | -| `ruff check` | 3x | 3,000 | 600 | -80% | -| `pytest` | 4x | 8,000 | 800 | -90% | -| `go test` | 3x | 6,000 | 600 | -90% | -| `docker ps` | 3x | 900 | 180 | -80% | -| **Total** | | **~118,000** | **~23,900** | **-80%** | - -> Estimates based on medium-sized TypeScript/Rust projects. Actual savings vary by project size. - -## Installation - -### Homebrew (recommended) - -```bash -brew install rtk -``` - -### Quick Install (Linux/macOS) - -```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -``` - -> Installs to `~/.local/bin`. Add to PATH if needed: -> ```bash -> echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc -> ``` +## Install ### Cargo ```bash -cargo install --git https://github.com/rtk-ai/rtk -``` - -### Pre-built Binaries - -Download from [releases](https://github.com/rtk-ai/rtk/releases): -- macOS: `rtk-x86_64-apple-darwin.tar.gz` / `rtk-aarch64-apple-darwin.tar.gz` -- Linux: `rtk-x86_64-unknown-linux-musl.tar.gz` / `rtk-aarch64-unknown-linux-gnu.tar.gz` -- Windows: `rtk-x86_64-pc-windows-msvc.zip` - -> **Windows users**: Extract the zip and place `rtk.exe` somewhere in your PATH (e.g. `C:\Users\\.local\bin`). Run RTK from **Command Prompt**, **PowerShell**, or **Windows Terminal** — do not double-click the `.exe` (it will flash and close). For the best experience, use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) where the full hook system works natively. See [Windows setup](#windows) below for details. - -### Verify Installation - -```bash -rtk --version # Should show "rtk 0.28.2" -rtk gain # Should show token savings stats -``` - -> **Name collision warning**: Another project named "rtk" (Rust Type Kit) exists on crates.io. If `rtk gain` fails, you have the wrong package. Use `cargo install --git` above instead. - -## Quick Start - -```bash -# 1. Install for your AI tool -rtk init -g # Claude Code / Copilot (default) -rtk init -g --gemini # Gemini CLI -rtk init -g --codex # Codex (OpenAI) -rtk init -g --agent cursor # Cursor -rtk init --agent windsurf # Windsurf -rtk init --agent cline # Cline / Roo Code -rtk init --agent kilocode # Kilo Code -rtk init --agent antigravity # Google Antigravity - -# 2. Restart your AI tool, then test -git status # Automatically rewritten to rtk git status -``` - -The hook transparently rewrites Bash commands (e.g., `git status` -> `rtk git status`) before execution. Claude never sees the rewrite, it just gets compressed output. - -**Important:** the hook only runs on Bash tool calls. Claude Code built-in tools like `Read`, `Grep`, and `Glob` do not pass through the Bash hook, so they are not auto-rewritten. To get RTK's compact output for those workflows, use shell commands (`cat`/`head`/`tail`, `rg`/`grep`, `find`) or call `rtk read`, `rtk grep`, or `rtk find` directly. - -## How It Works - -``` - Without rtk: With rtk: - - Claude --git status--> shell --> git Claude --git status--> RTK --> git - ^ | ^ | | - | ~2,000 tokens (raw) | | ~200 tokens | filter | - +-----------------------------------+ +------- (filtered) ---+----------+ -``` - -Four strategies applied per command type: - -1. **Smart Filtering** - Removes noise (comments, whitespace, boilerplate) -2. **Grouping** - Aggregates similar items (files by directory, errors by type) -3. **Truncation** - Keeps relevant context, cuts redundancy -4. **Deduplication** - Collapses repeated log lines with counts - -## Commands - -### Files -```bash -rtk ls . # Token-optimized directory tree -rtk read file.rs # Smart file reading -rtk read file.rs -l aggressive # Signatures only (strips bodies) -rtk smart file.rs # 2-line heuristic code summary -rtk find "*.rs" . # Compact find results -rtk grep "pattern" . # Grouped search results -rtk diff file1 file2 # Condensed diff -``` - -### Git -```bash -rtk git status # Compact status -rtk git log -n 10 # One-line commits -rtk git diff # Condensed diff -rtk git add # -> "ok" -rtk git commit -m "msg" # -> "ok abc1234" -rtk git push # -> "ok main" -rtk git pull # -> "ok 3 files +10 -2" -``` - -### GitHub CLI -```bash -rtk gh pr list # Compact PR listing -rtk gh pr view 42 # PR details + checks -rtk gh issue list # Compact issue listing -rtk gh run list # Workflow run status -``` - -### Test Runners -```bash -rtk jest # Jest compact (failures only) -rtk vitest # Vitest compact (failures only) -rtk playwright test # E2E results (failures only) -rtk pytest # Python tests (-90%) -rtk go test # Go tests (NDJSON, -90%) -rtk cargo test # Cargo tests (-90%) -rtk rake test # Ruby minitest (-90%) -rtk rspec # RSpec tests (JSON, -60%+) -rtk err # Filter errors only from any command -rtk test # Generic test wrapper - failures only (-90%) -``` - -### Build & Lint -```bash -rtk lint # ESLint grouped by rule/file -rtk lint biome # Supports other linters -rtk tsc # TypeScript errors grouped by file -rtk next build # Next.js build compact -rtk prettier --check . # Files needing formatting -rtk cargo build # Cargo build (-80%) -rtk cargo clippy # Cargo clippy (-80%) -rtk ruff check # Python linting (JSON, -80%) -rtk golangci-lint run # Go linting (JSON, -85%) -rtk rubocop # Ruby linting (JSON, -60%+) -``` - -### Package Managers -```bash -rtk pnpm list # Compact dependency tree -rtk pip list # Python packages (auto-detect uv) -rtk pip outdated # Outdated packages -rtk bundle install # Ruby gems (strip Using lines) -rtk prisma generate # Schema generation (no ASCII art) -``` - -### AWS -```bash -rtk aws sts get-caller-identity # One-line identity -rtk aws ec2 describe-instances # Compact instance list -rtk aws lambda list-functions # Name/runtime/memory (strips secrets) -rtk aws logs get-log-events # Timestamped messages only -rtk aws cloudformation describe-stack-events # Failures first -rtk aws dynamodb scan # Unwraps type annotations -rtk aws iam list-roles # Strips policy documents -rtk aws s3 ls # Truncated with tee recovery -``` - -### Containers -```bash -rtk docker ps # Compact container list -rtk docker images # Compact image list -rtk docker logs # Deduplicated logs -rtk docker compose ps # Compose services -rtk kubectl pods # Compact pod list -rtk kubectl logs # Deduplicated logs -rtk kubectl services # Compact service list -``` - -### Data & Analytics -```bash -rtk json config.json # Structure without values -rtk deps # Dependencies summary -rtk env -f AWS # Filtered env vars -rtk log app.log # Deduplicated logs -rtk curl # Truncate + save full output -rtk wget # Download, strip progress bars -rtk summary # Heuristic summary -rtk proxy # Raw passthrough + tracking -``` - -### Token Savings Analytics -```bash -rtk gain # Summary stats -rtk gain --graph # ASCII graph (last 30 days) -rtk gain --history # Recent command history -rtk gain --daily # Day-by-day breakdown -rtk gain --all --format json # JSON export for dashboards - -rtk discover # Find missed savings opportunities -rtk discover --all --since 7 # All projects, last 7 days - -rtk session # Show RTK adoption across recent sessions -``` - -## Global Flags - -```bash --u, --ultra-compact # ASCII icons, inline format (extra token savings) --v, --verbose # Increase verbosity (-v, -vv, -vvv) -``` - -## Examples - -**Directory listing:** -``` -# ls -la (45 lines, ~800 tokens) # rtk ls (12 lines, ~150 tokens) -drwxr-xr-x 15 user staff 480 ... my-project/ --rw-r--r-- 1 user staff 1234 ... +-- src/ (8 files) -... | +-- main.rs - +-- Cargo.toml -``` - -**Git operations:** -``` -# git push (15 lines, ~200 tokens) # rtk git push (1 line, ~10 tokens) -Enumerating objects: 5, done. ok main -Counting objects: 100% (5/5), done. -Delta compression using up to 8 threads -... -``` - -**Test output:** -``` -# cargo test (200+ lines on failure) # rtk test cargo test (~20 lines) -running 15 tests FAILED: 2/15 tests -test utils::test_parse ... ok test_edge_case: assertion failed -test utils::test_format ... ok test_overflow: panic at utils.rs:18 -... +cargo install --git https://github.com/duolingo/rtk ``` -## Auto-Rewrite Hook - -The most effective way to use rtk. The hook transparently intercepts Bash commands and rewrites them to rtk equivalents before execution. - -**Result**: 100% rtk adoption across all conversations and subagents, zero token overhead. - -**Scope note:** this only applies to Bash tool calls. Claude Code built-in tools such as `Read`, `Grep`, and `Glob` bypass the hook, so use shell commands or explicit `rtk` commands when you want RTK filtering there. - -### Setup +### Homebrew ```bash -rtk init -g # Install hook + RTK.md (recommended) -rtk init -g --opencode # OpenCode plugin (instead of Claude Code) -rtk init -g --auto-patch # Non-interactive (CI/CD) -rtk init -g --hook-only # Hook only, no RTK.md -rtk init --show # Verify installation +brew install rtk ``` -After install, **restart Claude Code**. - -## Windows - -RTK works on Windows with some limitations. The auto-rewrite hook (`rtk-rewrite.sh`) requires a Unix shell, so on native Windows RTK falls back to **CLAUDE.md injection mode** — your AI assistant receives RTK instructions but commands are not rewritten automatically. - -### Recommended: WSL (full support) - -For the best experience, use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) (Windows Subsystem for Linux). Inside WSL, RTK works exactly like Linux — full hook support, auto-rewrite, everything: +## Quick start ```bash -# Inside WSL -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh +# Install hooks rtk init -g -``` - -### Native Windows (limited support) -On native Windows (cmd.exe / PowerShell), RTK filters work but the hook does not auto-rewrite commands: - -```powershell -# 1. Download and extract rtk-x86_64-pc-windows-msvc.zip from releases -# 2. Add rtk.exe to your PATH -# 3. Initialize (falls back to CLAUDE.md injection) -rtk init -g -# 4. Use rtk explicitly -rtk cargo test +# Use filtered commands rtk git status +rtk cargo test +rtk grep "error" . ``` -**Important**: Do not double-click `rtk.exe` — it is a CLI tool that prints usage and exits immediately. Always run it from a terminal (Command Prompt, PowerShell, or Windows Terminal). - -| Feature | WSL | Native Windows | -|---------|-----|----------------| -| Filters (cargo, git, etc.) | Full | Full | -| Auto-rewrite hook | Yes | No (CLAUDE.md fallback) | -| `rtk init -g` | Hook mode | CLAUDE.md mode | -| `rtk gain` / analytics | Full | Full | - -## Supported AI Tools - -RTK supports 12 AI coding tools. Each integration transparently rewrites shell commands to `rtk` equivalents for 60-90% token savings. - -| Tool | Install | Method | -|------|---------|--------| -| **Claude Code** | `rtk init -g` | PreToolUse hook (bash) | -| **GitHub Copilot (VS Code)** | `rtk init -g --copilot` | PreToolUse hook — transparent rewrite | -| **GitHub Copilot CLI** | `rtk init -g --copilot` | PreToolUse deny-with-suggestion (CLI limitation) | -| **Cursor** | `rtk init -g --agent cursor` | preToolUse hook (hooks.json) | -| **Gemini CLI** | `rtk init -g --gemini` | BeforeTool hook | -| **Codex** | `rtk init -g --codex` | AGENTS.md + RTK.md instructions | -| **Windsurf** | `rtk init --agent windsurf` | .windsurfrules (project-scoped) | -| **Cline / Roo Code** | `rtk init --agent cline` | .clinerules (project-scoped) | -| **OpenCode** | `rtk init -g --opencode` | Plugin TS (tool.execute.before) | -| **OpenClaw** | `openclaw plugins install ./openclaw` | Plugin TS (before_tool_call) | -| **Mistral Vibe** | Planned ([#800](https://github.com/rtk-ai/rtk/issues/800)) | Blocked on upstream | -| **Kilo Code** | `rtk init --agent kilocode` | .kilocode/rules/rtk-rules.md (project-scoped) | -| **Google Antigravity** | `rtk init --agent antigravity` | .agents/rules/antigravity-rtk-rules.md (project-scoped) | - -For per-agent setup details, override controls, and graceful degradation, see the [Supported Agents guide](https://www.rtk-ai.app/guide/getting-started/supported-agents). - -## Configuration - -`~/.config/rtk/config.toml` (macOS: `~/Library/Application Support/rtk/config.toml`): - -```toml -[hooks] -exclude_commands = ["curl", "playwright"] # skip rewrite for these - -[tee] -enabled = true # save raw output on failure (default: true) -mode = "failures" # "failures", "always", or "never" -``` - -When a command fails, RTK saves the full unfiltered output so the LLM can read it without re-executing: +## Core commands -``` -FAILED: 2/15 tests -[full output: ~/.local/share/rtk/tee/1707753600_cargo_test.log] -``` +- Files: `rtk ls`, `rtk tree`, `rtk read`, `rtk find`, `rtk grep`, `rtk diff` +- Dev tools: `rtk cargo`, `rtk npm`, `rtk pnpm`, `rtk vitest`, `rtk pytest`, `rtk go test` +- Git/GitHub: `rtk git`, `rtk gh`, `rtk gt` +- Infra: `rtk aws`, `rtk docker`, `rtk kubectl` +- Passthrough: `rtk proxy ` -For the full config reference (all sections, env vars, per-project filters), see the [Configuration guide](https://www.rtk-ai.app/guide/getting-started/configuration). +## Privacy -### Uninstall - -```bash -rtk init -g --uninstall # Remove hook, RTK.md, settings.json entry -cargo uninstall rtk # Remove binary -brew uninstall rtk # If installed via Homebrew -``` +`rtk` in this fork does not send telemetry. Local `rtk gain` stats are stored only on disk and are never uploaded by RTK. ## Documentation -- **[rtk-ai.app/guide](https://www.rtk-ai.app/guide)** — full user guide (installation, supported agents, what gets optimized, analytics, configuration, troubleshooting) -- **[INSTALL.md](INSTALL.md)** — detailed installation reference -- **[ARCHITECTURE.md](ARCHITECTURE.md)** — system design and technical decisions -- **[CONTRIBUTING.md](CONTRIBUTING.md)** — contribution guide -- **[SECURITY.md](SECURITY.md)** — security policy - -## Privacy & Telemetry - -RTK can collect **anonymous, aggregate usage metrics** once per day. Telemetry is **disabled by default** and requires **explicit opt-in consent** (GDPR Art. 6, 7) during `rtk init` or via `rtk telemetry enable`. This data helps us build a better product: identifying which commands need filters, which filters need improvement, and how much value RTK delivers. For the full list of fields, data handling, and contributor guidelines, see **[docs/TELEMETRY.md](docs/TELEMETRY.md)**. - -**What is collected and why:** - -| Category | Data | Why | -|----------|------|-----| -| Identity | Salted device hash (SHA-256, not reversible) | Count unique installations without tracking individuals | -| Environment | RTK version, OS, architecture, install method | Know which platforms to support and test | -| Usage volume | Command count (24h), total commands, tokens saved (24h/30d/total) | Measure adoption and value delivered | -| Quality | Top 5 passthrough commands (0% savings), parse failure count, commands with <30% savings | Identify missing filters and weak ones to improve | -| Ecosystem | Command category distribution (e.g. git 45%, cargo 20%, js 15%) | Prioritize filter development for popular ecosystems | -| Retention | Days since first use, active days in last 30 | Understand engagement and detect churn | -| Adoption | AI agent hook type (claude/gemini/codex), custom TOML filter count | Track integration coverage and DSL adoption | -| Configuration | Whether config.toml exists, number of excluded commands, project count | Understand user maturity and customization patterns | -| Features | Usage counts for meta-commands (gain, discover, proxy, verify) | Know which RTK features are valued vs unused | -| Economics | Estimated USD savings (based on API token pricing) | Quantify the value RTK provides to users | - -All data is **aggregate counts or anonymized command names** (first 3 words, no arguments). Top commands report only tool names (e.g. "git", "cargo"), never full command lines. - -**What is NOT collected:** source code, file paths, command arguments, secrets, environment variables, personal data, or repository contents. - -**Manage telemetry:** -```bash -rtk telemetry status # Check current consent state -rtk telemetry enable # Give consent (interactive prompt) -rtk telemetry disable # Withdraw consent — stops all collection immediately -rtk telemetry forget # Withdraw consent + delete all local data + request server-side erasure -``` - -**Override via environment:** -```bash -export RTK_TELEMETRY_DISABLED=1 # Blocks telemetry regardless of consent -``` - -## Star History - - - - - - Star History Chart - - - -## StarMapper - - - - - - StarMapper - - - -## Core team - -- **Patrick Szymkowiak** — Founder - [GitHub](https://github.com/pszymkowiak) · [LinkedIn](https://www.linkedin.com/in/patrick-szymkowiak/) -- **Florian Bruniaux** — Core contributor - [GitHub](https://github.com/FlorianBruniaux) · [LinkedIn](https://www.linkedin.com/in/florian-bruniaux-43408b83/) -- **Adrien Eppling** — Core contributor - [GitHub](https://github.com/aeppling) · [LinkedIn](https://www.linkedin.com/in/adrien-eppling/) - -## Contributing - -Contributions welcome! Please open an issue or PR on [GitHub](https://github.com/rtk-ai/rtk). - -Join the community on [Discord](https://discord.gg/RySmvNF5kF). - -## License - -MIT License - see [LICENSE](LICENSE) for details. - -## Disclaimer - -See [DISCLAIMER.md](DISCLAIMER.md). +- Installation: `INSTALL.md` +- Troubleshooting: `docs/TROUBLESHOOTING.md` +- Architecture: `docs/contributing/ARCHITECTURE.md` diff --git a/README_es.md b/README_es.md deleted file mode 100644 index 751bd1ba8..000000000 --- a/README_es.md +++ /dev/null @@ -1,165 +0,0 @@ -

- RTK - Rust Token Killer -

- -

- Proxy CLI de alto rendimiento que reduce el consumo de tokens LLM en un 60-90% -

- -

- CI - Release - License: MIT - Discord - Homebrew -

- -

- Sitio web • - Instalar • - Solucion de problemas • - Arquitectura • - Discord -

- -

- English • - Francais • - 中文 • - 日本語 • - 한국어 • - Espanol -

- ---- - -rtk filtra y comprime las salidas de comandos antes de que lleguen al contexto de tu LLM. Binario Rust unico, cero dependencias, <10ms de overhead. - -## Ahorro de tokens (sesion de 30 min en Claude Code) - -| Operacion | Frecuencia | Estandar | rtk | Ahorro | -|-----------|------------|----------|-----|--------| -| `ls` / `tree` | 10x | 2,000 | 400 | -80% | -| `cat` / `read` | 20x | 40,000 | 12,000 | -70% | -| `grep` / `rg` | 8x | 16,000 | 3,200 | -80% | -| `git status` | 10x | 3,000 | 600 | -80% | -| `cargo test` / `npm test` | 5x | 25,000 | 2,500 | -90% | -| **Total** | | **~118,000** | **~23,900** | **-80%** | - -## Instalacion - -### Homebrew (recomendado) - -```bash -brew install rtk -``` - -### Instalacion rapida (Linux/macOS) - -```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -``` - -### Cargo - -```bash -cargo install --git https://github.com/rtk-ai/rtk -``` - -### Verificacion - -```bash -rtk --version # Debe mostrar "rtk 0.27.x" -rtk gain # Debe mostrar estadisticas de ahorro -``` - -## Inicio rapido - -```bash -# 1. Instalar hook para Claude Code (recomendado) -rtk init --global - -# 2. Reiniciar Claude Code, luego probar -git status # Automaticamente reescrito a rtk git status -``` - -## Como funciona - -``` - Sin rtk: Con rtk: - - Claude --git status--> shell --> git Claude --git status--> RTK --> git - ^ | ^ | | - | ~2,000 tokens (crudo) | | ~200 tokens | filtro | - +-----------------------------------+ +------- (filtrado) ---+----------+ -``` - -Cuatro estrategias: - -1. **Filtrado inteligente** - Elimina ruido (comentarios, espacios, boilerplate) -2. **Agrupacion** - Agrega elementos similares (archivos por directorio, errores por tipo) -3. **Truncamiento** - Mantiene contexto relevante, elimina redundancia -4. **Deduplicacion** - Colapsa lineas de log repetidas con contadores - -## Comandos - -### Archivos -```bash -rtk ls . # Arbol de directorios optimizado -rtk read file.rs # Lectura inteligente -rtk find "*.rs" . # Resultados compactos -rtk grep "pattern" . # Busqueda agrupada por archivo -``` - -### Git -```bash -rtk git status # Estado compacto -rtk git log -n 10 # Commits en una linea -rtk git diff # Diff condensado -rtk git push # -> "ok main" -``` - -### Tests -```bash -rtk jest # Jest compacto -rtk vitest # Vitest compacto -rtk pytest # Tests Python (-90%) -rtk go test # Tests Go (-90%) -rtk cargo test # Tests Rust (-90%) -rtk test # Solo fallos (-90%) -``` - -### Build & Lint -```bash -rtk lint # ESLint agrupado por regla -rtk tsc # Errores TypeScript agrupados -rtk cargo build # Build Cargo (-80%) -rtk ruff check # Lint Python (-80%) -``` - -### Analiticas -```bash -rtk gain # Estadisticas de ahorro -rtk gain --graph # Grafico ASCII (30 dias) -rtk discover # Descubrir ahorros perdidos -``` - -## Documentacion - -- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - Resolver problemas comunes -- **[INSTALL.md](INSTALL.md)** - Guia de instalacion detallada -- **[ARCHITECTURE.md](docs/contributing/ARCHITECTURE.md)** - Arquitectura tecnica - -## Contribuir - -Las contribuciones son bienvenidas. Abre un issue o PR en [GitHub](https://github.com/rtk-ai/rtk). - -Unete a la comunidad en [Discord](https://discord.gg/RySmvNF5kF). - -## Licencia - -Licencia MIT - ver [LICENSE](LICENSE) para detalles. - -## Descargo de responsabilidad - -Ver [DISCLAIMER.md](DISCLAIMER.md). diff --git a/README_fr.md b/README_fr.md deleted file mode 100644 index d305feaaf..000000000 --- a/README_fr.md +++ /dev/null @@ -1,202 +0,0 @@ -

- RTK - Rust Token Killer -

- -

- Proxy CLI haute performance qui reduit la consommation de tokens LLM de 60-90% -

- -

- CI - Release - License: MIT - Discord - Homebrew -

- -

- Site web • - Installer • - Depannage • - Architecture • - Discord -

- -

- English • - Francais • - 中文 • - 日本語 • - 한국어 • - Espanol -

- ---- - -rtk filtre et compresse les sorties de commandes avant qu'elles n'atteignent le contexte de votre LLM. Binaire Rust unique, zero dependance, <10ms d'overhead. - -## Economies de tokens (session Claude Code de 30 min) - -| Operation | Frequence | Standard | rtk | Economies | -|-----------|-----------|----------|-----|-----------| -| `ls` / `tree` | 10x | 2 000 | 400 | -80% | -| `cat` / `read` | 20x | 40 000 | 12 000 | -70% | -| `grep` / `rg` | 8x | 16 000 | 3 200 | -80% | -| `git status` | 10x | 3 000 | 600 | -80% | -| `git diff` | 5x | 10 000 | 2 500 | -75% | -| `git log` | 5x | 2 500 | 500 | -80% | -| `git add/commit/push` | 8x | 1 600 | 120 | -92% | -| `cargo test` / `npm test` | 5x | 25 000 | 2 500 | -90% | -| **Total** | | **~118 000** | **~23 900** | **-80%** | - -> Estimations basees sur des projets TypeScript/Rust de taille moyenne. - -## Installation - -### Homebrew (recommande) - -```bash -brew install rtk -``` - -### Installation rapide (Linux/macOS) - -```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -``` - -### Cargo - -```bash -cargo install --git https://github.com/rtk-ai/rtk -``` - -### Verification - -```bash -rtk --version # Doit afficher "rtk 0.27.x" -rtk gain # Doit afficher les statistiques d'economies -``` - -> **Attention** : Un autre projet "rtk" (Rust Type Kit) existe sur crates.io. Si `rtk gain` echoue, vous avez le mauvais package. - -## Demarrage rapide - -```bash -# 1. Installer le hook pour Claude Code (recommande) -rtk init --global -# Suivre les instructions pour enregistrer dans ~/.claude/settings.json - -# 2. Redemarrer Claude Code, puis tester -git status # Automatiquement reecrit en rtk git status -``` - -Le hook reecrit de maniere transparente les commandes (ex: `git status` -> `rtk git status`) avant execution. - -## Comment ca marche - -``` - Sans rtk : Avec rtk : - - Claude --git status--> shell --> git Claude --git status--> RTK --> git - ^ | ^ | | - | ~2 000 tokens (brut) | | ~200 tokens | filtre | - +-----------------------------------+ +------- (filtre) -----+----------+ -``` - -Quatre strategies appliquees par type de commande : - -1. **Filtrage intelligent** - Supprime le bruit (commentaires, espaces, boilerplate) -2. **Regroupement** - Agregat d'elements similaires (fichiers par dossier, erreurs par type) -3. **Troncature** - Conserve le contexte pertinent, coupe la redondance -4. **Deduplication** - Fusionne les lignes de log repetees avec compteurs - -## Commandes - -### Fichiers -```bash -rtk ls . # Arbre de repertoires optimise -rtk read file.rs # Lecture intelligente -rtk read file.rs -l aggressive # Signatures uniquement -rtk find "*.rs" . # Resultats compacts -rtk grep "pattern" . # Resultats groupes par fichier -rtk diff file1 file2 # Diff condense -``` - -### Git -```bash -rtk git status # Status compact -rtk git log -n 10 # Commits sur une ligne -rtk git diff # Diff condense -rtk git add # -> "ok" -rtk git commit -m "msg" # -> "ok abc1234" -rtk git push # -> "ok main" -``` - -### Tests -```bash -rtk jest # Jest compact -rtk vitest # Vitest compact -rtk pytest # Tests Python (-90%) -rtk go test # Tests Go (-90%) -rtk cargo test # Tests Cargo (-90%) -rtk test # Echecs uniquement (-90%) -``` - -### Build & Lint -```bash -rtk lint # ESLint groupe par regle -rtk tsc # Erreurs TypeScript groupees -rtk cargo build # Build Cargo (-80%) -rtk cargo clippy # Clippy (-80%) -rtk ruff check # Linting Python (-80%) -``` - -### Conteneurs -```bash -rtk docker ps # Liste compacte -rtk docker logs # Logs dedupliques -rtk kubectl pods # Pods compacts -``` - -### Analytics -```bash -rtk gain # Statistiques d'economies -rtk gain --graph # Graphique ASCII (30 jours) -rtk discover # Trouver les economies manquees -``` - -## Configuration - -```toml -# ~/.config/rtk/config.toml -[tracking] -database_path = "/chemin/custom.db" - -[hooks] -exclude_commands = ["curl", "playwright"] - -[tee] -enabled = true -mode = "failures" -``` - -## Documentation - -- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - Resoudre les problemes courants -- **[INSTALL.md](INSTALL.md)** - Guide d'installation detaille -- **[ARCHITECTURE.md](docs/contributing/ARCHITECTURE.md)** - Architecture technique - -## Contribuer - -Les contributions sont les bienvenues ! Ouvrez une issue ou une PR sur [GitHub](https://github.com/rtk-ai/rtk). - -Rejoignez la communaute sur [Discord](https://discord.gg/RySmvNF5kF). - -## Licence - -Licence MIT - voir [LICENSE](LICENSE) pour les details. - -## Avertissement - -Voir [DISCLAIMER.md](DISCLAIMER.md). diff --git a/README_ja.md b/README_ja.md deleted file mode 100644 index 23bf4412f..000000000 --- a/README_ja.md +++ /dev/null @@ -1,164 +0,0 @@ -

- RTK - Rust Token Killer -

- -

- LLM トークン消費を 60-90% 削減する高性能 CLI プロキシ -

- -

- CI - Release - License: MIT - Discord - Homebrew -

- -

- ウェブサイト • - インストール • - トラブルシューティング • - アーキテクチャ • - Discord -

- -

- English • - Francais • - 中文 • - 日本語 • - 한국어 • - Espanol -

- ---- - -rtk はコマンド出力を LLM コンテキストに届く前にフィルタリング・圧縮します。単一の Rust バイナリ、依存関係ゼロ、オーバーヘッド 10ms 未満。 - -## トークン節約(30分の Claude Code セッション) - -| 操作 | 頻度 | 標準 | rtk | 節約 | -|------|------|------|-----|------| -| `ls` / `tree` | 10x | 2,000 | 400 | -80% | -| `cat` / `read` | 20x | 40,000 | 12,000 | -70% | -| `grep` / `rg` | 8x | 16,000 | 3,200 | -80% | -| `git status` | 10x | 3,000 | 600 | -80% | -| `cargo test` / `npm test` | 5x | 25,000 | 2,500 | -90% | -| **合計** | | **~118,000** | **~23,900** | **-80%** | - -## インストール - -### Homebrew(推奨) - -```bash -brew install rtk -``` - -### クイックインストール(Linux/macOS) - -```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -``` - -### Cargo - -```bash -cargo install --git https://github.com/rtk-ai/rtk -``` - -### 確認 - -```bash -rtk --version # "rtk 0.27.x" と表示されるはず -rtk gain # トークン節約統計が表示されるはず -``` - -## クイックスタート - -```bash -# 1. Claude Code 用フックをインストール(推奨) -rtk init --global - -# 2. Claude Code を再起動してテスト -git status # 自動的に rtk git status に書き換え -``` - -## 仕組み - -``` - rtk なし: rtk あり: - - Claude --git status--> shell --> git Claude --git status--> RTK --> git - ^ | ^ | | - | ~2,000 tokens(生出力) | | ~200 tokens | フィルタ | - +-----------------------------------+ +------- (圧縮済)----+----------+ -``` - -4つの戦略: - -1. **スマートフィルタリング** - ノイズを除去(コメント、空白、ボイラープレート) -2. **グルーピング** - 類似項目を集約(ディレクトリ別ファイル、タイプ別エラー) -3. **トランケーション** - 関連コンテキストを保持、冗長性をカット -4. **重複排除** - 繰り返しログ行をカウント付きで統合 - -## コマンド - -### ファイル -```bash -rtk ls . # 最適化されたディレクトリツリー -rtk read file.rs # スマートファイル読み取り -rtk find "*.rs" . # コンパクトな検索結果 -rtk grep "pattern" . # ファイル別グループ化検索 -``` - -### Git -```bash -rtk git status # コンパクトなステータス -rtk git log -n 10 # 1行コミット -rtk git diff # 圧縮された diff -rtk git push # -> "ok main" -``` - -### テスト -```bash -rtk jest # Jest コンパクト -rtk vitest # Vitest コンパクト -rtk pytest # Python テスト(-90%) -rtk go test # Go テスト(-90%) -rtk test # 失敗のみ表示(-90%) -``` - -### ビルド & リント -```bash -rtk lint # ESLint ルール別グループ化 -rtk tsc # TypeScript エラーグループ化 -rtk cargo build # Cargo ビルド(-80%) -rtk ruff check # Python リント(-80%) -``` - -### 分析 -```bash -rtk gain # 節約統計 -rtk gain --graph # ASCII グラフ(30日間) -rtk discover # 見逃した節約機会を発見 -``` - -## ドキュメント - -- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - よくある問題の解決 -- **[INSTALL.md](INSTALL.md)** - 詳細インストールガイド -- **[ARCHITECTURE.md](docs/contributing/ARCHITECTURE.md)** - 技術アーキテクチャ - -## コントリビュート - -コントリビューション歓迎![GitHub](https://github.com/rtk-ai/rtk) で issue または PR を作成してください。 - -[Discord](https://discord.gg/RySmvNF5kF) コミュニティに参加。 - -## ライセンス - -MIT ライセンス - 詳細は [LICENSE](LICENSE) を参照。 - -## 免責事項 - -詳細は [DISCLAIMER.md](DISCLAIMER.md) を参照。 diff --git a/README_ko.md b/README_ko.md deleted file mode 100644 index a07a4590a..000000000 --- a/README_ko.md +++ /dev/null @@ -1,164 +0,0 @@ -

- RTK - Rust Token Killer -

- -

- LLM 토큰 소비를 60-90% 줄이는 고성능 CLI 프록시 -

- -

- CI - Release - License: MIT - Discord - Homebrew -

- -

- 웹사이트 • - 설치 • - 문제 해결 • - 아키텍처 • - Discord -

- -

- English • - Francais • - 中文 • - 日本語 • - 한국어 • - Espanol -

- ---- - -rtk는 명령 출력이 LLM 컨텍스트에 도달하기 전에 필터링하고 압축합니다. 단일 Rust 바이너리, 의존성 없음, 10ms 미만의 오버헤드. - -## 토큰 절약 (30분 Claude Code 세션) - -| 작업 | 빈도 | 표준 | rtk | 절약 | -|------|------|------|-----|------| -| `ls` / `tree` | 10x | 2,000 | 400 | -80% | -| `cat` / `read` | 20x | 40,000 | 12,000 | -70% | -| `grep` / `rg` | 8x | 16,000 | 3,200 | -80% | -| `git status` | 10x | 3,000 | 600 | -80% | -| `cargo test` / `npm test` | 5x | 25,000 | 2,500 | -90% | -| **합계** | | **~118,000** | **~23,900** | **-80%** | - -## 설치 - -### Homebrew (권장) - -```bash -brew install rtk -``` - -### 빠른 설치 (Linux/macOS) - -```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -``` - -### Cargo - -```bash -cargo install --git https://github.com/rtk-ai/rtk -``` - -### 확인 - -```bash -rtk --version # "rtk 0.27.x" 표시되어야 함 -rtk gain # 토큰 절약 통계 표시되어야 함 -``` - -## 빠른 시작 - -```bash -# 1. Claude Code용 hook 설치 (권장) -rtk init --global - -# 2. Claude Code 재시작 후 테스트 -git status # 자동으로 rtk git status로 재작성 -``` - -## 작동 원리 - -``` - rtk 없이: rtk 사용: - - Claude --git status--> shell --> git Claude --git status--> RTK --> git - ^ | ^ | | - | ~2,000 tokens (원본) | | ~200 tokens | 필터 | - +-----------------------------------+ +------- (필터링) -----+----------+ -``` - -네 가지 전략: - -1. **스마트 필터링** - 노이즈 제거 (주석, 공백, 보일러플레이트) -2. **그룹화** - 유사 항목 집계 (디렉토리별 파일, 유형별 에러) -3. **잘라내기** - 관련 컨텍스트 유지, 중복 제거 -4. **중복 제거** - 반복 로그 라인을 카운트와 함께 통합 - -## 명령어 - -### 파일 -```bash -rtk ls . # 최적화된 디렉토리 트리 -rtk read file.rs # 스마트 파일 읽기 -rtk find "*.rs" . # 컴팩트한 검색 결과 -rtk grep "pattern" . # 파일별 그룹화 검색 -``` - -### Git -```bash -rtk git status # 컴팩트 상태 -rtk git log -n 10 # 한 줄 커밋 -rtk git diff # 압축된 diff -rtk git push # -> "ok main" -``` - -### 테스트 -```bash -rtk jest # Jest 컴팩트 -rtk vitest # Vitest 컴팩트 -rtk pytest # Python 테스트 (-90%) -rtk go test # Go 테스트 (-90%) -rtk test # 실패만 표시 (-90%) -``` - -### 빌드 & 린트 -```bash -rtk lint # ESLint 규칙별 그룹화 -rtk tsc # TypeScript 에러 그룹화 -rtk cargo build # Cargo 빌드 (-80%) -rtk ruff check # Python 린트 (-80%) -``` - -### 분석 -```bash -rtk gain # 절약 통계 -rtk gain --graph # ASCII 그래프 (30일) -rtk discover # 놓친 절약 기회 발견 -``` - -## 문서 - -- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - 일반적인 문제 해결 -- **[INSTALL.md](INSTALL.md)** - 상세 설치 가이드 -- **[ARCHITECTURE.md](docs/contributing/ARCHITECTURE.md)** - 기술 아키텍처 - -## 기여 - -기여를 환영합니다! [GitHub](https://github.com/rtk-ai/rtk)에서 issue 또는 PR을 생성해 주세요. - -[Discord](https://discord.gg/RySmvNF5kF) 커뮤니티에 참여하세요. - -## 라이선스 - -MIT 라이선스 - 자세한 내용은 [LICENSE](LICENSE)를 참조하세요. - -## 면책 조항 - -자세한 내용은 [DISCLAIMER.md](DISCLAIMER.md)를 참조하세요. diff --git a/README_zh.md b/README_zh.md deleted file mode 100644 index 854ca2314..000000000 --- a/README_zh.md +++ /dev/null @@ -1,172 +0,0 @@ -

- RTK - Rust Token Killer -

- -

- 高性能 CLI 代理,将 LLM token 消耗降低 60-90% -

- -

- CI - Release - License: MIT - Discord - Homebrew -

- -

- 官网 • - 安装 • - 故障排除 • - 架构 • - Discord -

- -

- English • - Francais • - 中文 • - 日本語 • - 한국어 • - Espanol -

- ---- - -rtk 在命令输出到达 LLM 上下文之前进行过滤和压缩。单一 Rust 二进制文件,零依赖,<10ms 开销。 - -## Token 节省(30 分钟 Claude Code 会话) - -| 操作 | 频率 | 标准 | rtk | 节省 | -|------|------|------|-----|------| -| `ls` / `tree` | 10x | 2,000 | 400 | -80% | -| `cat` / `read` | 20x | 40,000 | 12,000 | -70% | -| `grep` / `rg` | 8x | 16,000 | 3,200 | -80% | -| `git status` | 10x | 3,000 | 600 | -80% | -| `git diff` | 5x | 10,000 | 2,500 | -75% | -| `cargo test` / `npm test` | 5x | 25,000 | 2,500 | -90% | -| **总计** | | **~118,000** | **~23,900** | **-80%** | - -## 安装 - -### Homebrew(推荐) - -```bash -brew install rtk -``` - -### 快速安装(Linux/macOS) - -```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -``` - -### Cargo - -```bash -cargo install --git https://github.com/rtk-ai/rtk -``` - -### 验证 - -```bash -rtk --version # 应显示 "rtk 0.27.x" -rtk gain # 应显示 token 节省统计 -``` - -## 快速开始 - -```bash -# 1. 为 Claude Code 安装 hook(推荐) -rtk init --global - -# 2. 重启 Claude Code,然后测试 -git status # 自动重写为 rtk git status -``` - -## 工作原理 - -``` - 没有 rtk: 使用 rtk: - - Claude --git status--> shell --> git Claude --git status--> RTK --> git - ^ | ^ | | - | ~2,000 tokens(原始) | | ~200 tokens | 过滤 | - +-----------------------------------+ +------- (已过滤)-----+----------+ -``` - -四种策略: - -1. **智能过滤** - 去除噪音(注释、空白、样板代码) -2. **分组** - 聚合相似项(按目录分文件,按类型分错误) -3. **截断** - 保留相关上下文,删除冗余 -4. **去重** - 合并重复日志行并计数 - -## 命令 - -### 文件 -```bash -rtk ls . # 优化的目录树 -rtk read file.rs # 智能文件读取 -rtk find "*.rs" . # 紧凑的查找结果 -rtk grep "pattern" . # 按文件分组的搜索结果 -``` - -### Git -```bash -rtk git status # 紧凑状态 -rtk git log -n 10 # 单行提交 -rtk git diff # 精简 diff -rtk git push # -> "ok main" -``` - -### 测试 -```bash -rtk jest # Jest 紧凑输出 -rtk vitest # Vitest 紧凑输出 -rtk pytest # Python 测试(-90%) -rtk go test # Go 测试(-90%) -rtk test # 仅显示失败(-90%) -``` - -### 构建 & 检查 -```bash -rtk lint # ESLint 按规则分组 -rtk tsc # TypeScript 错误分组 -rtk cargo build # Cargo 构建(-80%) -rtk ruff check # Python lint(-80%) -``` - -### 容器 -```bash -rtk docker ps # 紧凑容器列表 -rtk docker logs # 去重日志 -rtk kubectl pods # 紧凑 Pod 列表 -``` - -### 分析 -```bash -rtk gain # 节省统计 -rtk gain --graph # ASCII 图表(30 天) -rtk discover # 发现遗漏的节省机会 -``` - -## 文档 - -- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - 解决常见问题 -- **[INSTALL.md](INSTALL.md)** - 详细安装指南 -- **[ARCHITECTURE.md](docs/contributing/ARCHITECTURE.md)** - 技术架构 - -## 贡献 - -欢迎贡献!请在 [GitHub](https://github.com/rtk-ai/rtk) 上提交 issue 或 PR。 - -加入 [Discord](https://discord.gg/RySmvNF5kF) 社区。 - -## 许可证 - -MIT 许可证 - 详见 [LICENSE](LICENSE)。 - -## 免责声明 - -详见 [DISCLAIMER.md](DISCLAIMER.md)。 diff --git a/SECURITY.md b/SECURITY.md index 0b7f3a9be..49120ce3b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,217 +1,17 @@ # Security Policy -## Reporting a Vulnerability +## Reporting -If you discover a security vulnerability in RTK, please report it to the maintainers privately: +Report vulnerabilities via GitHub Security Advisories for this repository. -- **Email**: security@rtk-ai.app (or create a private security advisory on GitHub) -- **Response time**: We aim to acknowledge reports within 48 hours -- **Disclosure**: We follow responsible disclosure practices (90-day embargo) +## Current security baseline -**Please do NOT:** -- Open public GitHub issues for security vulnerabilities -- Disclose vulnerabilities on social media or forums before we've had a chance to address them +- `cargo audit` in CI +- Dependabot updates for Cargo and GitHub Actions +- PR dependency review (`actions/dependency-review-action`) +- dependency graph submission workflow (`cargo-lock-submission`) ---- +## Non-goals in this fork -## Security Review Process for Pull Requests - -RTK is a CLI tool that executes shell commands and handles user input. PRs from external contributors undergo enhanced security review to protect against: - -- **Shell injection** (command execution vulnerabilities) -- **Supply chain attacks** (malicious dependencies) -- **Backdoors** (logic bombs, exfiltration code) -- **Data leaks** (tracking.db exposure, telemetry abuse) - ---- - -## Automated Security Checks - -Every PR triggers our [`security-check.yml`](.github/workflows/security-check.yml) workflow: - -1. **Dependency audit** (`cargo audit`) - Detects known CVEs -2. **Critical files alert** - Flags modifications to high-risk files -3. **Dangerous pattern scan** - Regex-based detection of: - - Shell execution (`Command::new("sh")`) - - Environment manipulation (`.env("LD_PRELOAD")`) - - Network operations (`reqwest::`, `std::net::`) - - Unsafe code blocks - - Panic-inducing patterns (`.unwrap()` in production) -4. **Clippy security lints** - Enforces Rust best practices - -Results are posted in the PR's GitHub Actions summary. - ---- - -## Critical Files Requiring Enhanced Review - -The following files are considered **high-risk** and trigger mandatory 2-reviewer approval: - -### Tier 1: Shell Execution & System Interaction -- **`src/runner.rs`** - Shell command execution engine (primary injection vector) -- **`src/summary.rs`** - Command output aggregation (data exfiltration risk) -- **`src/tracking.rs`** - SQLite database operations (privacy/telemetry concerns) -- **`src/discover/registry.rs`** - Rewrite logic for all commands (command injection risk via rewrite rules) -- **`hooks/rtk-rewrite.sh`** / **`.claude/hooks/rtk-rewrite.sh`** - Thin delegator hook (executes in Claude Code context, intercepts all commands) - -### Tier 2: Input Validation -- **`src/pnpm_cmd.rs`** - Package name validation (prevents injection via malicious names) -- **`src/container.rs`** - Docker/container operations (privilege escalation risk) - -### Tier 3: Supply Chain & CI/CD -- **`Cargo.toml`** - Dependency manifest (typosquatting, backdoored crates) -- **`.github/workflows/*.yml`** - CI/CD pipelines (release tampering, secret exfiltration) - -**If your PR modifies ANY of these files**, expect: -- Detailed manual security review -- Request for clarification on design choices -- Potentially slower merge timeline - ---- - -## Review Workflow - -### For External Contributors - -1. **Submit PR** → Automated `security-check.yml` runs -2. **Review automated results** → Fix any flagged issues -3. **Manual review** → Maintainer performs comprehensive security audit -4. **Approval** → Merge (or request for changes) - -### For Maintainers - -Use the comprehensive security review process: - -```bash -# If Claude Code available, run the dedicated skill: -/rtk-pr-security - -# Manual review (without Claude): -gh pr view -gh pr diff > /tmp/pr.diff -bash scripts/detect-dangerous-patterns.sh /tmp/pr.diff -``` - -**Review checklist:** -- [ ] No critical files modified OR changes justified + reviewed by 2 maintainers -- [ ] No dangerous patterns OR patterns explained + safe -- [ ] No new dependencies OR deps audited on crates.io (downloads, maintainer, license) -- [ ] PR description matches actual code changes (intent vs reality) -- [ ] No logic bombs (time-based triggers, conditional backdoors) -- [ ] Code quality acceptable (no unexplained complexity spikes) - ---- - -## Dangerous Patterns We Check For - -| Pattern | Risk | Example | -|---------|------|---------| -| `Command::new("sh")` | Shell injection | Spawns shell with user input | -| `.env("LD_PRELOAD")` | Library hijacking | Preloads malicious shared libraries | -| `reqwest::`, `std::net::` | Data exfiltration | Unexpected network operations | -| `unsafe {` | Memory safety | Bypasses Rust's guarantees | -| `.unwrap()` in `src/` | DoS via panic | Crashes on invalid input | -| `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. - ---- - -## Dependency Security - -New dependencies added to `Cargo.toml` must meet these criteria: - -- **Downloads**: >10,000 on crates.io (or strong justification if lower) -- **Maintainer**: Verified GitHub profile + track record of other crates -- **License**: MIT or Apache-2.0 compatible -- **Activity**: Recent commits (within 6 months) -- **No typosquatting**: Manual verification against similar crate names - -**Red flags:** -- Brand new crate (<1 month old) with low downloads -- Anonymous maintainer with no GitHub history -- Crate name suspiciously similar to popular crate (e.g., `serid` vs `serde`) -- License change in recent versions - ---- - -## Security Best Practices for Contributors - -### Avoid These Anti-Patterns - -**❌ DON'T:** -```rust -// Shell injection risk -let user_input = get_arg(); -Command::new("sh").arg("-c").arg(format!("echo {}", user_input)).output(); - -// Panic on invalid input -let path = std::env::args().nth(1).unwrap(); - -// Hardcoded secrets -const API_KEY: &str = "sk_live_1234567890abcdef"; -``` - -**✅ DO:** -```rust -// No shell, direct binary execution -let user_input = get_arg(); -Command::new("echo").arg(user_input).output(); - -// Graceful error handling -let path = std::env::args().nth(1).context("Missing path argument")?; - -// Env vars or config files for secrets -let api_key = std::env::var("API_KEY").context("API_KEY not set")?; -``` - -### Error Handling Guidelines - -- Use `anyhow::Result` with `.context()` for all error propagation -- NEVER use `.unwrap()` in `src/` (tests are OK) -- Prefer `.expect("descriptive message")` over `.unwrap()` if unavoidable -- Use `?` operator instead of `unwrap()` for propagation - -### Input Validation - -- Validate all user input before passing to `Command` -- Use allowlists for command flags (not denylists) -- Canonicalize file paths to prevent traversal attacks -- Sanitize package names with strict regex patterns - ---- - -## Disclosure Timeline - -When vulnerabilities are reported: - -1. **Day 0**: Acknowledgment sent to reporter -2. **Day 7**: Maintainers assess severity and impact -3. **Day 14**: Patch development begins -4. **Day 30**: Patch released + CVE filed (if applicable) -5. **Day 90**: Public disclosure (or earlier if patch is deployed) - -Critical vulnerabilities (remote code execution, data exfiltration) may be fast-tracked. - ---- - -## Security Tooling - -- **`cargo audit`** - Automated CVE scanning (runs in CI) -- **`cargo deny`** - License compliance + banned dependencies -- **`cargo clippy`** - Lints for unsafe patterns -- **GitHub Dependabot** - Automated dependency updates -- **GitHub Code Scanning** - Static analysis via CodeQL (planned) - ---- - -## Contact - -- **Security issues**: security@rtk-ai.app -- **General questions**: https://github.com/rtk-ai/rtk/discussions -- **Maintainers**: @FlorianBruniaux (active fork maintainer) - ---- - -**Last updated**: 2026-03-05 +- no outbound telemetry +- no outbound analytics upload; local stats stay local diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md deleted file mode 100644 index 1bded7163..000000000 --- a/docs/TELEMETRY.md +++ /dev/null @@ -1,189 +0,0 @@ -# Telemetry - -RTK collects anonymous, aggregate usage metrics once per day to help improve the product. Telemetry is **disabled by default** and requires explicit consent during `rtk init` or `rtk telemetry enable`. - -## Data Collector - -**Entity**: `RTK AI Labs` -**Contact**: contact@rtk-ai.app - -## Why we collect telemetry - -RTK supports 100+ commands across 15+ ecosystems. Without telemetry, we have no way to know: - -- Which commands are used most and need the best filters -- Which filters are underperforming and need improvement -- Which ecosystems to prioritize for new filter development -- How much value RTK delivers to users (token savings in $ terms) -- Whether users stay engaged over time or churn after trying RTK - -This data directly drives our roadmap. For example, if telemetry shows that 40% of users run Python commands but only 10% of our filters cover Python, we know where to invest next. - -## How it works - -1. **Once per day** (23-hour interval), RTK sends a single HTTPS POST to our telemetry endpoint -2. The ping runs in a **background thread** and never blocks the CLI (2-second timeout) -3. A marker file prevents duplicate pings within the interval -4. If the server is unreachable, the ping is silently dropped — no retries, no queue - -**Source code**: [`src/core/telemetry.rs`](../src/core/telemetry.rs) - -## What is collected - -### Identity (anonymous) - -| Field | Example | Purpose | -|-------|---------|---------| -| `device_hash` | `a3f8c9...` (64 hex chars) | Count unique installations. SHA-256 of a per-device random salt stored locally (`~/.local/share/rtk/.device_salt`). Not reversible. No hostname or username included. | - -### Environment - -| Field | Example | Purpose | -|-------|---------|---------| -| `version` | `0.34.1` | Track adoption of new versions | -| `os` | `macos` | Know which platforms to support and test | -| `arch` | `aarch64` | Prioritize ARM vs x86 builds | -| `install_method` | `homebrew` | Understand distribution channels (homebrew/cargo/script/nix) | - -### Usage volume - -| Field | Example | Purpose | -|-------|---------|---------| -| `commands_24h` | `142` | Daily activity level | -| `commands_total` | `32888` | Lifetime usage — segment light vs heavy users | -| `top_commands` | `["git", "cargo", "ls"]` | Most popular tools (names only, max 5) | -| `tokens_saved_24h` | `450000` | Daily value delivered | -| `tokens_saved_total` | `96500000` | Lifetime value delivered | -| `savings_pct` | `72.5` | Overall effectiveness | - -### Quality (filter improvement) - -| Field | Example | Purpose | -|-------|---------|---------| -| `passthrough_top` | `["git:15", "npm:8"]` | Top 5 commands with 0% savings — these need filters | -| `parse_failures_24h` | `3` | Filter fragility — high count means filters are breaking | -| `low_savings_commands` | `["rtk docker ps:25%"]` | Commands averaging <30% savings — filters to improve | -| `avg_savings_per_command` | `68.5` | Unweighted average (vs global which is volume-biased) | - -### Ecosystem distribution - -| Field | Example | Purpose | -|-------|---------|---------| -| `ecosystem_mix` | `{"git": 45, "cargo": 20, "js": 15}` | Category percentages — where to invest filter development | - -### Retention (engagement) - -| Field | Example | Purpose | -|-------|---------|---------| -| `first_seen_days` | `45` | Installation age in days | -| `active_days_30d` | `22` | Days with at least 1 command in last 30 days — measures stickiness | - -### Economics - -| Field | Example | Purpose | -|-------|---------|---------| -| `tokens_saved_30d` | `12000000` | 30-day token savings for trend analysis | -| `estimated_savings_usd_30d` | `36.0` | Estimated dollar value saved (at ~$3/Mtok input pricing, Claude Sonnet) | - -### Adoption - -| Field | Example | Purpose | -|-------|---------|---------| -| `hook_type` | `claude` | Which AI agent hook is installed (claude/gemini/codex/cursor/none) | -| `custom_toml_filters` | `3` | Number of user-created TOML filter files — DSL adoption | - -### Configuration (user maturity) - -| Field | Example | Purpose | -|-------|---------|---------| -| `has_config_toml` | `true` | Whether user has customized RTK config | -| `exclude_commands_count` | `2` | Commands excluded from rewriting — high count may indicate frustration | -| `projects_count` | `5` | Distinct project paths — multi-project = power user | - -### Feature adoption - -| Field | Example | Purpose | -|-------|---------|---------| -| `meta_usage` | `{"gain": 5, "discover": 2}` | Which RTK features are actually used | - -## What is NOT collected - -- Source code or file contents -- Full command lines or arguments (only tool names like "git", "cargo") -- File paths or directory structures -- Secrets, API keys, or environment variable values -- Repository names or URLs -- Personally identifiable information -- IP addresses (not stored in telemetry pings; stored temporarily in erasure audit log for accountability, anonymized after 6 months) - -## Consent - -Telemetry requires explicit opt-in consent (GDPR Art. 6, 7). Consent is requested during `rtk init` or via `rtk telemetry enable`. Without consent, no data is sent. - -```bash -rtk telemetry status # Check current consent state -rtk telemetry enable # Give consent (interactive prompt) -rtk telemetry disable # Withdraw consent -rtk telemetry forget # Withdraw consent + delete local data + request server erasure -``` - -Environment variable override (blocks telemetry regardless of consent): -```bash -export RTK_TELEMETRY_DISABLED=1 -``` - -## Retention Policy - -- **Server-side**: telemetry records are retained for a maximum of **12 months**, then automatically purged (periodic task every 24 hours). -- **Server-side (erasure log)**: IP addresses in the erasure audit log are **anonymized after 6 months** (GDPR — IP is personal data). -- **Client-side**: the local SQLite database (`~/.local/share/rtk/tracking.db`) retains data for **90 days** by default (configurable via `tracking.history_days` in `config.toml`). Deleted entirely by `rtk telemetry forget`. - -## Your Rights (GDPR) - -Under the EU General Data Protection Regulation, you have the right to: - -- **Access** your data: `rtk telemetry status` shows your device hash; the telemetry payload is fully documented above. -- **Rectification**: since data is anonymous and aggregate, rectification is not applicable. -- **Erasure** (Art. 17): run `rtk telemetry forget` to delete local data and send an erasure request to the server. Alternatively, email contact@rtk-ai.app with your device hash. -- **Restriction of processing**: `rtk telemetry disable` stops all data collection immediately. -- **Portability**: the local SQLite database at `~/.local/share/rtk/tracking.db` contains all locally stored data. -- **Objection**: `rtk telemetry disable` or `export RTK_TELEMETRY_DISABLED=1`. - -## Erasure Procedure - -1. Run `rtk telemetry forget` — this disables telemetry, deletes your device salt, ping marker, and local tracking database (`history.db`), then sends an erasure request to the server. -2. If the server is unreachable, the CLI prints your full device hash and fallback instructions to email contact@rtk-ai.app for manual erasure. -3. You can also email contact@rtk-ai.app directly to request manual erasure. - -## Data Handling - -- Telemetry endpoint URL and auth token are injected at **compile time** via `option_env!()` — they are not in the source code -- All communications use HTTPS (TLS) -- Data is used exclusively for RTK product improvement -- No data is sold or shared with third parties -- Aggregate statistics may be published (e.g. "70% of RTK users are on macOS") - -### Server-side Requirements - -The telemetry server must implement: -- `POST /erasure` endpoint accepting `{"device_hash": "...", "action": "erasure"}`, authenticated via `X-RTK-Token` -- Automatic periodic purge of telemetry records older than 12 months -- Audit log for erasure requests (GDPR Art. 17(2) accountability) with IP anonymization after 6 months - -## For contributors - -The telemetry implementation lives in `src/core/telemetry.rs`. Key design decisions: - -- **Fire-and-forget**: errors are silently ignored, never shown to users -- **Non-blocking**: runs in a `std::thread::spawn`, 2-second timeout -- **No async**: consistent with RTK's single-threaded design -- **Compile-time gating**: if `RTK_TELEMETRY_URL` is not set at build time, all telemetry code is dead — the binary makes zero network calls -- **23-hour interval**: prevents clock-drift accumulation that a strict 24h interval would cause - -When adding new fields: -1. Add the query method to `src/core/tracking.rs` -2. Add the field to `EnrichedStats` in `src/core/telemetry.rs` -3. Populate it in `get_enriched_stats()` -4. Add it to the JSON payload in `send_ping()` -5. Update this document and the README.md privacy table -6. Ensure the field contains only **aggregate counts or anonymized names** — no raw paths, arguments, or user data diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 000000000..54418930c --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,20 @@ +# Troubleshooting + +## `rtk` command not found + +Ensure your install path is on `PATH`. + +```bash +rtk --version +rtk --help +``` + +## Hook rewrite not happening + +Re-run setup: + +```bash +rtk init -g +``` + +Then restart your agent tool. diff --git a/docs/contributing/ARCHITECTURE.md b/docs/contributing/ARCHITECTURE.md index 72d288766..ee80f72d5 100644 --- a/docs/contributing/ARCHITECTURE.md +++ b/docs/contributing/ARCHITECTURE.md @@ -1,1060 +1,15 @@ -# rtk Architecture Documentation +# Architecture -> **Deep reference** for RTK's system design, filtering taxonomy, performance characteristics, and architecture decisions. For a guided tour of the end-to-end flow, start with [TECHNICAL.md](TECHNICAL.md). +Main components: -**rtk (Rust Token Killer)** is a high-performance CLI proxy that minimizes LLM token consumption through intelligent output filtering and compression. +- `src/main.rs`: CLI and command routing +- `src/cmds/*`: command filters and formatters +- `src/core/*`: shared utilities (config, filtering, runner, tee) +- `src/hooks/*`: hook install and integrity checks +- `src/discover/*`: rewrite registry and parsing helpers used by hooks ---- +Removed from this fork: -## Table of Contents - -1. [System Overview](#system-overview) -2. [Command Lifecycle](#command-lifecycle) -3. [Module Organization](#module-organization) -4. [Filtering Strategies](#filtering-strategies) -5. [Shared Infrastructure](#shared-infrastructure) -6. [Token Tracking System](#token-tracking-system) -7. [Global Flags Architecture](#global-flags-architecture) -8. [Error Handling](#error-handling) -9. [Configuration System](#configuration-system) -10. [Common Patterns](#common-patterns) -11. [Build Optimizations](#build-optimizations) -12. [Extensibility Guide](#extensibility-guide) -13. [Architecture Decision Records](#architecture-decision-records) - ---- - -## System Overview - -> For the proxy pattern diagram and key components table, see [TECHNICAL.md](TECHNICAL.md#2-architecture-overview). - -### Design Principles - -1. **Single Responsibility**: Each module handles one command type -2. **Minimal Overhead**: ~5-15ms proxy overhead per command -3. **Exit Code Preservation**: CI/CD reliability through proper exit code propagation -4. **Fail-Safe**: If filtering fails, fall back to original output -5. **Transparent**: Users can always see raw output with `-v` flags - -### Hook Architecture (v0.9.5+) - -> For the hook interception diagram and agent-specific JSON formats, see [TECHNICAL.md](TECHNICAL.md#32-hook-interception-command-rewriting) and [hooks/README.md](hooks/README.md). - -Two hook strategies: - -``` -Auto-Rewrite (default) Suggest (non-intrusive) -───────────────────── ──────────────────────── -Hook intercepts command Hook emits systemMessage hint -Rewrites before execution Claude decides autonomously -100% adoption ~70-85% adoption -Zero context overhead Minimal context overhead -Best for: production Best for: learning / auditing -``` - ---- - -## Command Lifecycle - -### Six-Phase Execution Flow - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Command Execution Lifecycle │ -└────────────────────────────────────────────────────────────────────────┘ - -Phase 1: PARSE -────────────── -$ rtk git log --oneline -5 -v - -Clap Parser extracts: - • Command: Commands::Git - • Args: ["log", "--oneline", "-5"] - • Flags: verbose = 1 - ultra_compact = false - - ↓ - -Phase 2: ROUTE -────────────── -main.rs:match Commands::Git { args, .. } - ↓ -git::run(args, verbose) - - ↓ - -Phase 3: EXECUTE -──────────────── -std::process::Command::new("git") - .args(["log", "--oneline", "-5"]) - .output()? - -Output captured: - • stdout: "abc123 Fix bug\ndef456 Add feature\n..." (500 chars) - • stderr: "" (empty) - • exit_code: 0 - - ↓ - -Phase 4: FILTER -─────────────── -git::format_git_output(stdout, "log", verbose) - -Strategy: Stats Extraction - • Count commits: 5 - • Extract stats: +142/-89 - • Compress: "5 commits, +142/-89" - -Filtered: 20 chars (96% reduction) - - ↓ - -Phase 5: PRINT -────────────── -if verbose > 0 { - eprintln!("Git log summary:"); // Debug -} -println!("{}", colored_output); // User output - -Terminal shows: "5 commits, +142/-89 ✓" - - ↓ - -Phase 6: TRACK -────────────── -tracking::track( - original_cmd: "git log --oneline -5", - rtk_cmd: "rtk git log --oneline -5", - input: &raw_output, // 500 chars - output: &filtered // 20 chars -) - - ↓ - -SQLite INSERT: - • input_tokens: 125 (500 / 4) - • output_tokens: 5 (20 / 4) - • savings_pct: 96.0 - • timestamp: now() - -Database: ~/.local/share/rtk/history.db -``` - -### Verbosity Levels - -``` --v (Level 1): Show debug messages - Example: eprintln!("Git log summary:"); - --vv (Level 2): Show command being executed - Example: eprintln!("Executing: git log --oneline -5"); - --vvv (Level 3): Show raw output before filtering - Example: eprintln!("Raw output:\n{}", stdout); -``` - ---- - -## Module Organization - -### Module Map - -> For the full file-level module tree, see [TECHNICAL.md](TECHNICAL.md#4-folder-map) and each folder's README. - -**Token savings by ecosystem:** - -``` -Savings by ecosystem: - GIT (cmds/git/) 85-99% status, diff, log, gh, gt - JS/TS (cmds/js/) 70-99% lint, tsc, next, prettier, playwright, prisma, vitest, pnpm - PYTHON (cmds/python/) 70-90% ruff, pytest, mypy, pip - GO (cmds/go/) 75-90% go test/build/vet, golangci-lint - RUBY (cmds/ruby/) 60-90% rake, rspec, rubocop - DOTNET (cmds/dotnet/) 70-85% dotnet build/test, binlog - CLOUD (cmds/cloud/) 60-80% aws, docker/kubectl, curl, wget, psql - SYSTEM (cmds/system/) 50-90% ls, tree, read, grep, find, json, log, env, deps - RUST (cmds/rust/) 60-99% cargo test/build/clippy, err -``` - -**Total: 64 modules** (42 command modules + 22 infrastructure modules) - -### Module Breakdown - -- **Command Modules**: `src/cmds/` — organized by ecosystem (git, rust, js, python, go, dotnet, cloud, system, ruby). Each ecosystem README lists its files. -- **Core Infrastructure**: `src/core/` — utils, filter, tracking, tee, config, toml_filter, display_helpers, telemetry -- **Hook System**: `src/hooks/` — init, rewrite, permissions, hook_cmd, hook_check, hook_audit, verify, trust, integrity -- **Analytics**: `src/analytics/` — gain, cc_economics, ccusage, session_cmd - -### Module Count Breakdown - -- **Command Modules**: 42 (directly exposed to users) -- **Infrastructure Modules**: 22 (utils, filter, tracking, tee, config, init, gain, toml_filter, verify_cmd, 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) -- **Go Tooling**: 2 modules (go test/build/vet, golangci-lint) - ---- - -## Filtering Strategies - -### Strategy Matrix - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Filtering Strategy Taxonomy │ -└────────────────────────────────────────────────────────────────────────┘ - -Strategy Modules Technique Reduction -────────────────────────────────────────────────────────────────────────── - -1. STATS EXTRACTION - ┌──────────────┐ - │ Raw: 5000 │ → Count/aggregate → "3 files, +142/-89" 90-99% - │ lines │ Drop details - └──────────────┘ - - Used by: git status, git log, git diff, pnpm list - -2. ERROR ONLY - ┌──────────────┐ - │ stdout+err │ → stderr only → "Error: X failed" 60-80% - │ Mixed │ Drop stdout - └──────────────┘ - - Used by: runner (err mode), test failures - -3. GROUPING BY PATTERN - ┌──────────────┐ - │ 100 errors │ → Group by rule → "no-unused-vars: 23" 80-90% - │ Scattered │ Count/summarize "semi: 45" - └──────────────┘ - - Used by: lint, tsc, grep (group by file/rule/error code) - -4. DEDUPLICATION - ┌──────────────┐ - │ Repeated │ → Unique + count → "[ERROR] ... (×5)" 70-85% - │ Log lines │ - └──────────────┘ - - Used by: log_cmd (identify patterns, count occurrences) - -5. STRUCTURE ONLY - ┌──────────────┐ - │ JSON with │ → Keys + types → {user: {...}, ...} 80-95% - │ Large values │ Strip values - └──────────────┘ - - Used by: json_cmd (schema extraction) - -6. CODE FILTERING - ┌──────────────┐ - │ Source code │ → Filter by level: - │ │ • none → Keep all 0% - │ │ • minimal → Strip comments 20-40% - │ │ • aggressive → Strip bodies 60-90% - └──────────────┘ - - Used by: read, smart (language-aware stripping via filter.rs) - -7. FAILURE FOCUS - ┌──────────────┐ - │ 100 tests │ → Failures only → "2 failed:" 94-99% - │ Mixed │ Hide passing " • test_auth" - └──────────────┘ - - Used by: vitest, playwright, runner (test mode) - -8. TREE COMPRESSION - ┌──────────────┐ - │ Flat list │ → Tree hierarchy → "src/" 50-70% - │ 50 files │ Aggregate dirs " ├─ lib/ (12)" - └──────────────┘ - - Used by: ls (directory tree with counts) - -9. PROGRESS FILTERING - ┌──────────────┐ - │ ANSI bars │ → Strip progress → "✓ Downloaded" 85-95% - │ Live updates │ Final result - └──────────────┘ - - Used by: wget, pnpm install (strip ANSI escape sequences) - -10. JSON/TEXT DUAL MODE - ┌──────────────┐ - │ Tool output │ → JSON when available → Structured data 80%+ - │ │ Text otherwise Fallback parse - └──────────────┘ - - Used by: ruff (check → JSON, format → text), pip (list/show → JSON) - -11. STATE MACHINE PARSING - ┌──────────────┐ - │ Test output │ → Track test state → "2 failed, 18 ok" 90%+ - │ Mixed format │ Extract failures Failure details - └──────────────┘ - - Used by: pytest (text state machine: test_name → PASSED/FAILED) - -12. NDJSON STREAMING - ┌──────────────┐ - │ Line-by-line │ → Parse each JSON → "2 fail (pkg1, pkg2)" 90%+ - │ JSON events │ Aggregate results Compact summary - └──────────────┘ - - Used by: go test (NDJSON stream, interleaved package events) -``` - -### Code Filtering Levels (src/core/filter.rs) - -```rust -// FilterLevel::None - Keep everything -fn calculate_total(items: &[Item]) -> i32 { - // Sum all items - items.iter().map(|i| i.value).sum() -} - -// FilterLevel::Minimal - Strip comments only (20-40% reduction) -fn calculate_total(items: &[Item]) -> i32 { - items.iter().map(|i| i.value).sum() -} - -// FilterLevel::Aggressive - Strip comments + function bodies (60-90% reduction) -fn calculate_total(items: &[Item]) -> i32 { ... } -``` - -**Language Support**: Rust, Python, JavaScript, TypeScript, Go, C, C++, Java - -**Detection**: File extension-based with fallback heuristics - ---- - -## Python & Go Module Architecture - -### Design Rationale - -**Added**: 2026-02-12 (v0.15.1) -**Motivation**: Complete language ecosystem coverage beyond JS/TS - -Python and Go modules follow distinct architectural patterns optimized for their ecosystems: - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Python vs Go Module Design │ -└────────────────────────────────────────────────────────────────────────┘ - -PYTHON (Standalone Commands) GO (Sub-Enum Pattern) -────────────────────────── ───────────────────── - -Commands::Ruff { args } ────── Commands::Go { -Commands::Pytest { args } Test { args }, -Commands::Pip { args } Build { args }, - Vet { args } - } -├─ ruff_cmd.rs Commands::GolangciLint { args } -├─ pytest_cmd.rs │ -└─ pip_cmd.rs ├─ go_cmd.rs (sub-enum router) - └─ golangci_cmd.rs - -Mirrors: lint, prettier Mirrors: git, cargo -``` - -### Python Stack Architecture - -#### Command Implementations - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Python Commands │ -└────────────────────────────────────────────────────────────────────────┘ - -Module Strategy Output Format Savings -───────────────────────────────────────────────────────────────────────── - -ruff_cmd.rs JSON/TEXT DUAL • check → JSON 80%+ - • format → text - - ruff check: JSON API with structured violations - { - "violations": [{"rule": "F401", "file": "x.py", "line": 5}] - } - → Group by rule, count occurrences - - ruff format: Text diff output - "Fixed 12 files" - → Extract summary, hide unchanged files - -pytest_cmd.rs STATE MACHINE Text parser 90%+ - - State tracking: IDLE → TEST_START → PASSED/FAILED → SUMMARY - Extract: - • Test names (test_auth_login) - • Outcomes (PASSED ✓ / FAILED ✗) - • Failures only (hide passing tests) - -pip_cmd.rs JSON PARSING JSON API 70-85% - - pip list --format=json: - [{"name": "requests", "version": "2.28.1"}] - → Compact table format - - pip show : JSON metadata - {"name": "...", "version": "...", "requires": [...]} - → Extract key fields only - - Auto-detect uv: If uv exists, use uv pip instead -``` - -#### Shared Infrastructure - -**No Package Manager Detection** -Unlike JS/TS modules, Python commands don't auto-detect poetry/pipenv/pip because: -- `pip` is universally available (system Python) -- `uv` detection is explicit (binary presence check) -- Poetry/pipenv aren't execution wrappers (they manage virtualenvs differently) - -**Virtual Environment Awareness** -Commands respect active virtualenv via `sys.executable` paths. - -### Go Stack Architecture - -#### Command Implementations - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Go Commands │ -└────────────────────────────────────────────────────────────────────────┘ - -Module Strategy Output Format Savings -───────────────────────────────────────────────────────────────────────── - -go_cmd.rs SUB-ENUM ROUTER Mixed formats 75-90% - - go test: NDJSON STREAMING - {"Action": "run", "Package": "pkg1", "Test": "TestAuth"} - {"Action": "fail", "Package": "pkg1", "Test": "TestAuth"} - - → Line-by-line JSON parse (handles interleaved package events) - → Aggregate: "2 packages, 3 failures (pkg1::TestAuth, ...)" - - go build: TEXT FILTERING - Errors only (compiler diagnostics) - → Strip warnings, show errors with file:line - - go vet: TEXT FILTERING - Issue detection output - → Extract file:line:message triples - -golangci_cmd.rs JSON PARSING JSON API 85% - - golangci-lint run --out-format=json: - { - "Issues": [ - {"FromLinter": "errcheck", "Pos": {...}, "Text": "..."} - ] - } - → Group by linter rule, count violations - → Format: "errcheck: 12 issues, gosec: 5 issues" -``` - -#### Sub-Enum Pattern (go_cmd.rs) - -Uses `Commands::Go { #[command(subcommand)] command: GoCommand }` in main.rs, with `GoCommand` enum routing to `run_test/run_build/run_vet`. Mirrors git/cargo patterns. - -**Why Sub-Enum?** -- `go test/build/vet` are semantically related (core Go toolchain) -- Mirrors existing git/cargo patterns (consistency) -- Natural CLI: `rtk go test` not `rtk gotest` - -**Why golangci-lint Standalone?** -- Third-party tool (not core Go toolchain) -- Different output format (JSON API vs text) -- Distinct use case (comprehensive linting vs single-tool diagnostics) - -### Ruby Module Architecture - -**Added**: 2026-03-15 -**Motivation**: Ruby on Rails development support (minitest, RSpec, RuboCop, Bundler) - -Ruby modules follow the standalone command pattern (like Python) with a shared `ruby_exec()` utility for auto-detecting `bundle exec`. - -``` -Module Strategy Output Format Savings -───────────────────────────────────────────────────────────────────────── -rake_cmd.rs STATE MACHINE Text parser 85-90% - Minitest output (rake test / rails test) - → State machine: Header → Running → Failures → Summary - → All pass: "ok rake test: 8 runs, 0 failures" - → Failures: summary + numbered failure details - -rspec_cmd.rs JSON/TEXT DUAL JSON → 60%+ 60%+ - Injects --format json, parses structured results - → Fallback to text state machine when JSON unavailable - → Strips Spring, SimpleCov, DEPRECATION, Capybara noise - -rubocop_cmd.rs JSON PARSING JSON API 60%+ - Injects --format json, groups by cop/severity - → Skips JSON injection in autocorrect mode (-a, -A) - -bundle-install.toml TOML FILTER Text rules 90%+ - → Strips "Using" lines, short-circuits to "ok bundle: complete" -``` - -**Shared**: `ruby_exec(tool)` in utils.rs auto-detects `bundle exec` when `Gemfile` exists. Used by rake_cmd, rspec_cmd, rubocop_cmd. - -### Format Strategy Decision Tree - -``` -Output format known? -├─ Tool provides JSON flag? -│ ├─ Structured data needed? → Use JSON API -│ │ Examples: ruff check, pip list, golangci-lint -│ │ -│ └─ Simple output? → Use text mode -│ Examples: ruff format, go build errors -│ -├─ Streaming events (NDJSON)? -│ └─ Line-by-line JSON parse -│ Examples: go test (interleaved packages) -│ -└─ Plain text only? - ├─ Stateful parsing needed? → State machine - │ Examples: pytest (test lifecycle tracking) - │ - └─ Simple filtering? → Text filters - Examples: go vet, go build -``` - -### Performance Characteristics - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Python/Go Module Overhead Benchmarks │ -└────────────────────────────────────────────────────────────────────────┘ - -Command Raw Time rtk Time Overhead Savings -───────────────────────────────────────────────────────────────────────── - -ruff check 850ms 862ms +12ms 83% -pytest 1.2s 1.21s +10ms 92% -pip list 450ms 458ms +8ms 78% - -go test 2.1s 2.12s +20ms 88% -go build (errors) 950ms 961ms +11ms 80% -golangci-lint 4.5s 4.52s +20ms 85% - -Overhead Sources: - • JSON parsing: 5-10ms (serde_json) - • State machine: 3-8ms (regex + state tracking) - • NDJSON streaming: 8-15ms (line-by-line JSON parse) -``` - -### Module Integration Checklist - -When adding Python/Go module support: - -- [x] **Output Format**: JSON API > NDJSON > State Machine > Text Filters -- [x] **Failure Focus**: Hide passing tests, show failures only -- [x] **Exit Code Preservation**: Propagate tool exit codes for CI/CD -- [x] **Virtual Env Awareness**: Python modules respect active virtualenv -- [x] **Error Grouping**: Group by rule/file for linters (ruff, golangci-lint) -- [x] **Streaming Support**: Handle interleaved NDJSON events (go test) -- [x] **Verbosity Levels**: Support -v/-vv/-vvv for debug output -- [x] **Token Tracking**: Integrate with tracking::track() -- [x] **Unit Tests**: Test parsing logic with representative outputs - ---- - -## Shared Infrastructure - -### Utilities Layer - -> For the full utilities API (`truncate`, `strip_ansi`, `execute_command`, `ruby_exec`, etc.), see [src/core/README.md](src/core/README.md). Used by most command modules. - -### Package Manager Detection Pattern - -**Critical Infrastructure for JS/TS Stack** - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Package Manager Detection Flow │ -└────────────────────────────────────────────────────────────────────────┘ - -Detection Order: -┌─────────────────────────────────────┐ -│ 1. Check: pnpm-lock.yaml exists? │ -│ → Yes: pnpm exec -- │ -│ │ -│ 2. Check: yarn.lock exists? │ -│ → Yes: yarn exec -- │ -│ │ -│ 3. Fallback: Use npx │ -│ → npx --no-install -- │ -└─────────────────────────────────────┘ - -Example (lint_cmd.rs:50-77): - -let is_pnpm = Path::new("pnpm-lock.yaml").exists(); -let is_yarn = Path::new("yarn.lock").exists(); - -let mut cmd = if is_pnpm { - Command::new("pnpm").arg("exec").arg("--").arg("eslint") -} else if is_yarn { - Command::new("yarn").arg("exec").arg("--").arg("eslint") -} else { - Command::new("npx").arg("--no-install").arg("--").arg("eslint") -}; - -Affects: lint, tsc, next, prettier, playwright, prisma, vitest, pnpm -``` - -**Why This Matters**: -- **CWD Preservation**: pnpm/yarn exec preserve working directory correctly -- **Monorepo Support**: Works in nested package.json structures -- **No Global Installs**: Uses project-local dependencies only -- **CI/CD Reliability**: Consistent behavior across environments - ---- - -## Token Tracking System - -### SQLite-Based Metrics - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Token Tracking Architecture │ -└────────────────────────────────────────────────────────────────────────┘ - -Flow: - -1. ESTIMATION (tracking.rs:235-238) - ──────────── - estimate_tokens(text: &str) → usize { - (text.len() as f64 / 4.0).ceil() as usize - } - - Heuristic: ~4 characters per token (GPT-style tokenization) - - ↓ - -2. CALCULATION - ─────────── - input_tokens = estimate_tokens(raw_output) - output_tokens = estimate_tokens(filtered_output) - saved_tokens = input_tokens - output_tokens - savings_pct = (saved / input) × 100.0 - - ↓ - -3. RECORD (tracking.rs:48-59) - ────── - INSERT INTO commands ( - timestamp, -- RFC3339 format - original_cmd, -- "git log --oneline -5" - rtk_cmd, -- "rtk git log --oneline -5" - input_tokens, -- 125 - output_tokens, -- 5 - saved_tokens, -- 120 - savings_pct, -- 96.0 - exec_time_ms -- 15 (execution duration in milliseconds) - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - - ↓ - -4. STORAGE - ─────── - Database: ~/.local/share/rtk/history.db - - Schema: - ┌─────────────────────────────────────────┐ - │ commands │ - ├─────────────────────────────────────────┤ - │ id INTEGER PRIMARY KEY │ - │ timestamp TEXT NOT NULL │ - │ original_cmd TEXT NOT NULL │ - │ rtk_cmd TEXT NOT NULL │ - │ input_tokens INTEGER NOT NULL │ - │ output_tokens INTEGER NOT NULL │ - │ saved_tokens INTEGER NOT NULL │ - │ savings_pct REAL NOT NULL │ - │ exec_time_ms INTEGER DEFAULT 0 │ - └─────────────────────────────────────────┘ - - Note: exec_time_ms tracks command execution duration - (added in v0.7.1, historical records default to 0) - - ↓ - -5. CLEANUP (tracking.rs:96-104) - ─────── - Auto-cleanup on each INSERT: - DELETE FROM commands - WHERE timestamp < datetime('now', '-90 days') - - Retention: 90 days (HISTORY_DAYS constant) - - ↓ - -6. REPORTING (gain.rs) - ──────── - $ rtk gain - - Query: - SELECT - COUNT(*) as total_commands, - SUM(saved_tokens) as total_saved, - AVG(savings_pct) as avg_savings, - SUM(exec_time_ms) as total_time_ms, - AVG(exec_time_ms) as avg_time_ms - FROM commands - WHERE timestamp > datetime('now', '-90 days') - - Output: - ┌──────────────────────────────────────┐ - │ Token Savings Report (90 days) │ - ├──────────────────────────────────────┤ - │ Commands executed: 1,234 │ - │ Average savings: 78.5% │ - │ Total tokens saved: 45,678 │ - │ Total exec time: 8m50s (573ms) │ - │ │ - │ Top commands: │ - │ • rtk git status (234 uses) │ - │ • rtk lint (156 uses) │ - │ • rtk test (89 uses) │ - └──────────────────────────────────────┘ - - Note: Time column shows average execution - duration per command (added in v0.7.1) -``` - -### Thread Safety - -Single-threaded execution with `Mutex>` for future-proofing. No multi-threading currently, but safe concurrent access is possible if needed. - ---- - -## Global Flags Architecture - -### Verbosity System - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Verbosity Levels │ -└────────────────────────────────────────────────────────────────────────┘ - -main.rs:47-49 -#[arg(short, long, action = clap::ArgAction::Count, global = true)] -verbose: u8, - -Levels: -┌─────────┬──────────────────────────────────────────────────────┐ -│ Flag │ Behavior │ -├─────────┼──────────────────────────────────────────────────────┤ -│ (none) │ Compact output only │ -│ -v │ + Debug messages (eprintln! statements) │ -│ -vv │ + Command being executed │ -│ -vvv │ + Raw output before filtering │ -└─────────┴──────────────────────────────────────────────────────┘ - -Example (git.rs:67-69): -if verbose > 0 { - eprintln!("Git diff summary:"); -} -``` - -### Ultra-Compact Mode - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Ultra-Compact Mode (-u) │ -└────────────────────────────────────────────────────────────────────────┘ - -main.rs:51-53 -#[arg(short = 'u', long, global = true)] -ultra_compact: bool, - -Features: -┌──────────────────────────────────────────────────────────────────────┐ -│ • ASCII icons instead of words (✓ ✗ → ⚠) │ -│ • Inline formatting (single-line summaries) │ -│ • Maximum compression for LLM contexts │ -└──────────────────────────────────────────────────────────────────────┘ - -Example (gh_cmd.rs:521): -if ultra_compact { - println!("✓ PR #{} merged", number); -} else { - println!("Pull request #{} successfully merged", number); -} -``` - ---- - -## Error Handling - -### anyhow::Result<()> Propagation Chain - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Error Handling Architecture │ -└────────────────────────────────────────────────────────────────────────┘ - -Propagation Chain: - -main() → Result<()> - ↓ - match cli.command { - Commands::Git { args, .. } => git::run(&args, verbose)?, - ... - } - ↓ .context("Git command failed") -git::run(args: &[String], verbose: u8) → Result<()> - ↓ .context("Failed to execute git") -git::execute_git_command() → Result - ↓ .context("Git process error") -Command::new("git").output()? - ↓ Error occurs -anyhow::Error - ↓ Bubble up through ? -main.rs error display - ↓ -eprintln!("Error: {:#}", err) - ↓ -std::process::exit(1) -``` - -### Exit Code Preservation (Critical for CI/CD) - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Exit Code Handling Strategy │ -└────────────────────────────────────────────────────────────────────────┘ - -Standard Pattern (git.rs:45-48, PR #5): - -let output = Command::new("git").args(args).output()?; - -if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - eprintln!("{}", stderr); - std::process::exit(output.status.code().unwrap_or(1)); -} - -Exit Codes: -┌─────────┬──────────────────────────────────────────────────────┐ -│ Code │ Meaning │ -├─────────┼──────────────────────────────────────────────────────┤ -│ 0 │ Success │ -│ 1 │ rtk internal error (parsing, filtering, etc.) │ -│ N │ Preserved exit code from underlying tool │ -│ │ (e.g., git returns 128, lint returns 1) │ -└─────────┴──────────────────────────────────────────────────────┘ - -Why This Matters: -• CI/CD pipelines rely on exit codes to determine build success/failure -• Pre-commit hooks need accurate failure signals -• Git workflows require proper exit code propagation (PR #5 fix) - -Modules with Exit Code Preservation: -• git.rs (all git commands) -• lint_cmd.rs (linter failures) -• tsc_cmd.rs (TypeScript errors) -• vitest_cmd.rs (test failures) -• playwright_cmd.rs (E2E test failures) -``` - ---- - -## Configuration System - -### Configuration - -> For config file format, tee settings, tracking database path, and TOML filter tiers, see [src/core/README.md](src/core/README.md). - -Two tiers: **User settings** (`~/.config/rtk/config.toml`) and **LLM integration** (CLAUDE.md via `rtk init`). - -### Initialization Flow - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ rtk init Workflow │ -└────────────────────────────────────────────────────────────────────────┘ - -$ rtk init [--global] - ↓ -Check existing CLAUDE.md: - • --global? → ~/.config/rtk/CLAUDE.md - • else → ./CLAUDE.md - ↓ - ├─ Exists? → Warn user, ask to overwrite - └─ Not exists? → Continue - ↓ -Prompt: "Initialize rtk for LLM usage? [y/N]" - ↓ Yes -Write template: -┌─────────────────────────────────────┐ -│ # CLAUDE.md │ -│ │ -│ Use `rtk` prefix for commands: │ -│ - rtk git status │ -│ - rtk lint │ -│ - rtk test │ -│ │ -│ Benefits: 60-90% token reduction │ -└─────────────────────────────────────┘ - ↓ -Success: "✓ Initialized rtk for LLM integration" -``` - ---- - -## Common Patterns - -#### 1. Package Manager Detection (JS/TS modules) - -```rust -// Detect lockfiles -let is_pnpm = Path::new("pnpm-lock.yaml").exists(); -let is_yarn = Path::new("yarn.lock").exists(); - -// Build command -let mut cmd = if is_pnpm { - Command::new("pnpm").arg("exec").arg("--").arg("eslint") -} else if is_yarn { - Command::new("yarn").arg("exec").arg("--").arg("eslint") -} else { - Command::new("npx").arg("--no-install").arg("--").arg("eslint") -}; -``` - -#### 2. Verbosity Guards - -```rust -if verbose > 0 { - eprintln!("Debug: Processing {} files", count); -} - -if verbose >= 2 { - eprintln!("Executing: {:?}", cmd); -} - -if verbose >= 3 { - eprintln!("Raw output:\n{}", raw); -} -``` - ---- - -## Build Optimizations - -### Release Profile (Cargo.toml) - -```toml -[profile.release] -opt-level = 3 # Maximum optimization -lto = true # Link-time optimization -codegen-units = 1 # Single codegen unit for better optimization -strip = true # Remove debug symbols -panic = "abort" # Smaller binary size -``` - -### Performance Characteristics - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Performance Metrics │ -└────────────────────────────────────────────────────────────────────────┘ - -Binary: - • Size: ~4.1 MB (stripped release build) - • Startup: ~5-10ms (cold start) - • Memory: ~2-5 MB (typical usage) - -Runtime Overhead (estimated): -┌──────────────────────┬──────────────┬──────────────┐ -│ Operation │ rtk Overhead │ Total Time │ -├──────────────────────┼──────────────┼──────────────┤ -│ rtk git status │ +8ms │ 58ms │ -│ rtk grep "pattern" │ +12ms │ 145ms │ -│ rtk read file.rs │ +5ms │ 15ms │ -│ rtk lint │ +15ms │ 2.5s │ -└──────────────────────┴──────────────┴──────────────┘ - -Note: Overhead measurements are estimates. Actual performance varies -by system, command complexity, and output size. - -Overhead Sources: - • Clap parsing: ~2-3ms - • Command execution: ~1-2ms - • Filtering/compression: ~2-8ms (varies by strategy) - • SQLite tracking: ~1-3ms -``` - ---- - -## Extensibility Guide - -> For the complete step-by-step process to add a new command (module file, enum variant, routing, tests, documentation), see [src/cmds/README.md — Adding a New Command Filter](src/cmds/README.md#adding-a-new-command-filter). - ---- - -## Architecture Decision Records - -### Why Rust? - -- **Performance**: ~5-15ms overhead per command (negligible for user experience) -- **Safety**: No runtime errors from null pointers, data races, etc. -- **Single Binary**: No runtime dependencies (distribute one executable) -- **Cross-Platform**: Works on macOS, Linux, Windows without modification - -### Why SQLite for Tracking? - -- **Zero Config**: No server setup, works out-of-the-box -- **Lightweight**: ~100KB database for 90 days of history -- **Reliable**: ACID compliance for data integrity -- **Queryable**: Rich analytics via SQL (gain report) - -### Why anyhow for Error Handling? - -- **Context**: `.context()` adds meaningful error messages throughout call chain -- **Ergonomic**: `?` operator for concise error propagation -- **User-Friendly**: Error display shows full context chain - -### Why Clap for CLI Parsing? - -- **Derive Macros**: Less boilerplate (declarative CLI definition) -- **Auto-Generated Help**: `--help` generated automatically -- **Type Safety**: Parse arguments directly into typed structs -- **Global Flags**: `-v` and `-u` work across all commands - ---- - -## Resources - -- **[TECHNICAL.md](TECHNICAL.md)**: Guided tour of end-to-end flow -- **[CONTRIBUTING.md](CONTRIBUTING.md)**: Design philosophy, contribution workflow, checklist -- **CLAUDE.md**: Quick reference for AI agents (dev commands, build verification) -- **README.md**: User guide, installation, examples -- **Cargo.toml**: Dependencies, build profiles, package metadata - ---- - -## Glossary - -| Term | Definition | -|------|------------| -| **Token** | Unit of text processed by LLMs (~4 characters on average) | -| **Filtering** | Reducing output size while preserving essential information | -| **Proxy Pattern** | rtk sits between user and tool, transforming output | -| **Exit Code Preservation** | Passing through tool's exit code for CI/CD reliability | -| **Package Manager Detection** | Identifying pnpm/yarn/npm to execute JS/TS tools correctly | -| **Verbosity Levels** | `-v/-vv/-vvv` for progressively more debug output | -| **Ultra-Compact** | `-u` flag for maximum compression (ASCII icons, inline format) | - ---- - -**Last Updated**: 2026-03-24 -**Architecture Version**: 3.1 +- OpenClaw integration +- telemetry sender (removed in this fork) +- analytics/tracking commands (removed in this fork) diff --git a/docs/contributing/TECHNICAL.md b/docs/contributing/TECHNICAL.md index ddadf8d73..dd4f7617b 100644 --- a/docs/contributing/TECHNICAL.md +++ b/docs/contributing/TECHNICAL.md @@ -1,432 +1,11 @@ -# RTK Technical Documentation +# Technical Notes -> **Start here** for a guided tour of how RTK works end-to-end. -> -> - [CONTRIBUTING.md](../CONTRIBUTING.md) — Design philosophy, PR process, branch naming, testing requirements -> - [ARCHITECTURE.md](ARCHITECTURE.md) — Deep reference: filtering taxonomy, performance benchmarks, architecture decisions -> - Each folder has its own `README.md` with implementation details and file descriptions +`rtk` is a command proxy. ---- +Flow: +1. Parse CLI args. +2. Route to command-specific filter module. +3. Execute underlying command. +4. Return filtered output. -## 1. Project Vision - -LLM-powered coding agents (Claude Code, Copilot, Cursor, etc.) consume tokens for every CLI command output they process. Most command outputs contain boilerplate, progress bars, ANSI escape codes, and verbose formatting that wastes tokens without providing actionable information. - -RTK sits between the agent and the CLI, filtering outputs to keep only what matters. This achieves 60-90% token savings per command, reducing costs and increasing effective context window utilization. RTK is a single Rust binary with no runtime dependencies beyond the compiled binary itself, adding less than 10ms overhead per command. - ---- - -## 2. Architecture Overview - -``` -User / LLM Agent - | - v -+--------------------------------------------------+ -| LLM Agent Hook | -| hooks/{claude,copilot,cursor,...}/ | -| Intercepts: "git status" -> "rtk git status" | -+-------------------------+------------------------+ - | - v -+--------------------------------------------------+ -| RTK CLI (main.rs) | -| | -| +-------------+ +-----------------+ | -| | Clap Parser | -> | Command Routing | | -| | (Commands | | (match on enum) | | -| | enum) | +--------+--------+ | -| +-------------+ | | -| +---------+---------+ | -| v v v | -| +----------+ +--------+ +----------+| -| |Rust Filter| |TOML DSL| |Passthru || -| |(cmds/**) | |Filter | |(fallback)|| -| +-----+----+ +----+---+ +----+-----+| -| | | | | -| +-----+-----+-----------+ | -| v | -| +---------------------+ | -| | Token Tracking | | -| | (core/tracking) | | -| | SQLite DB | | -| +---------------------+ | -+--------------------------------------------------+ -``` - -**Design principles:** -- Single-threaded, no async (startup < 10ms) -- Graceful degradation: filter failure falls back to raw output -- Exit code propagation: RTK never swallows non-zero exits -- Transparent proxy: unknown commands pass through unchanged - ---- - -## 3. End-to-End Flow - -This is the full lifecycle of a command through RTK, from LLM agent to filtered output. - -### 3.1 Hook Installation (`rtk init`) - -The user runs `rtk init` to set up hooks for their LLM agent. This: - -1. Writes a thin shell hook script (e.g., `~/.claude/hooks/rtk-rewrite.sh`) -2. Stores its SHA-256 hash for integrity verification -3. Patches the agent's settings file (e.g., `settings.json`) to register the hook -4. Writes RTK awareness instructions (e.g., `RTK.md`) for prompt-level guidance - -RTK supports 7 agents, each with its own installation mode. The hook scripts are embedded in the binary and written at install time. - -> **Details**: [`src/hooks/README.md`](../src/hooks/README.md) covers all installation modes, configuration files, and the uninstall flow. - -### 3.2 Hook Interception (Command Rewriting) - -When an LLM agent runs a command (e.g., `git status`): - -1. The agent fires a `PreToolUse` event (or equivalent) containing the command as JSON -2. The hook script reads the JSON, extracts the command string -3. The hook calls `rtk rewrite "git status"` as a subprocess -4. `rtk rewrite` consults the command registry and returns `rtk git status` -5. The hook sends a response telling the agent to use the rewritten command -6. If anything fails (jq missing, rtk not found, no match), the hook exits silently -- the raw command runs unchanged - -All rewrite logic lives in Rust (`src/discover/registry.rs`). Hooks are thin delegates that handle agent-specific JSON formats. - -> **Details**: [`hooks/README.md`](../hooks/README.md) covers each agent's JSON format, the rewrite registry, compound command handling, and the `RTK_DISABLED` override. - -#### Rewrite Pipeline - -The rewrite pipeline is how RTK intercepts and rewrites commands. The call chain is: - -``` -hook shell → rewrite_cmd.rs → rewrite_command() → rewrite_compound() → rewrite_segment() → classify_command() -``` - -Traced step by step for `cargo fmt --all && cargo test 2>&1 | tail -20`: - -``` -LLM Agent: "cargo fmt --all && cargo test 2>&1 | tail -20" - | - | Hook shell (hooks/claude/rtk-rewrite.sh) - | Reads JSON from agent, extracts command, calls `rtk rewrite "$CMD"` - | On failure (jq missing, rtk missing, old version): exit 0 (passthrough) - | - v -rewrite_cmd::run(cmd) [src/hooks/rewrite_cmd.rs] - | 1. Load config → hooks.exclude_commands - | 2. check_command(cmd) → Deny → exit(2) - | 3. registry::rewrite_command(cmd, excluded) - | → None → exit(1) (no RTK equivalent, passthrough) - | → Some + Allow → print, exit(0) - | → Some + Ask → print, exit(3) - | - v -rewrite_command(cmd, excluded) [src/discover/registry.rs] - | Early exits: - | - Empty → None - | - Contains "<<" or "$((" (heredoc/arithmetic) → None - | - Simple "rtk ..." (no operators) → return as-is - | - Otherwise → rewrite_compound(cmd, excluded) - | - v -rewrite_compound(cmd, excluded) [src/discover/registry.rs] - | - | Step 1 — Tokenize (lexer.rs) - | tokenize() produces typed tokens with byte offsets: - | Arg("cargo") Arg("fmt") Arg("--all") - | Operator("&&") - | Arg("cargo") Arg("test") Redirect("2>&1") - | Pipe("|") - | Arg("tail") Arg("-20") - | - | Step 2 — Split on operators, rewrite each segment - | Operator (&&, ||, ;) → rewrite both sides - | Pipe (|) → rewrite left side only, keep right side raw - | exception: find/fd before pipe → skip rewrite - | Shellism (&) → rewrite both sides (background) - | - | Calls rewrite_segment() per segment: - | segment 1: "cargo fmt --all" - | segment 2: "cargo test 2>&1" - | after pipe: "tail -20" kept raw - | - v -rewrite_segment(seg, excluded) [src/discover/registry.rs] - | - | Step 3 — Strip trailing redirects - | strip_trailing_redirects() re-tokenizes the segment: - | "cargo test 2>&1" → cmd_part="cargo test", redirect=" 2>&1" - | (simple commands like "cargo fmt --all" → no redirect, suffix is "") - | - | Step 4 — Already RTK → return as-is - | - | Step 5 — Special cases (short-circuit before classification) - | head -N / --lines=N → rewrite_line_range() → "rtk read file --max-lines N" - | tail -N / -n N / --lines N → rewrite_line_range() → "rtk read file --tail-lines N" - | head/tail with unsupported flag (-c, -f) → None (skip rewrite) - | cat with incompatible flag (-A, -v, -e) → None (skip rewrite) - | - | Step 6 — classify_command(cmd_part) [see below] - | → Supported → check excluded list → continue - | → Unsupported/Ignored → None (skip rewrite) - | - | Step 7 — Build rewritten command - | a. Find matching rule from rules.rs - | b. Extract env prefix (ENV_PREFIX regex, second pass — first was in classify) - | e.g. "GIT_SSH_COMMAND=\"ssh -o ...\" git push" → prefix="GIT_SSH_COMMAND=..." - | c. Guard: RTK_DISABLED=1 in prefix → None - | d. Guard: gh with --json/--jq/--template → None - | e. Apply rule's rewrite_prefixes: "cargo fmt" → "rtk cargo fmt" - | f. Reassemble: env_prefix + rtk_cmd + args + redirect_suffix - | - v -classify_command(cmd) [src/discover/registry.rs] - | 1. Check IGNORED_EXACT (cd, echo, fi, done, ...) - | 2. Check IGNORED_PREFIXES (rtk, mkdir, mv, ...) - | 3. Strip env prefix with ENV_PREFIX regex (for pattern matching only) - | 4. Normalize absolute paths: /usr/bin/grep → grep - | 5. Strip git global opts: git -C /tmp status → git status - | 6. Guard: cat/head/tail with redirect (>, >>) → Unsupported (write, not read) - | 7. Match against REGEX_SET (60+ compiled patterns from rules.rs) - | 8. Extract subcommand → lookup custom savings/status overrides - | 9. Return Classification::Supported { rtk_equivalent, category, savings, status } - | - v -Result: "rtk cargo fmt --all && rtk cargo test 2>&1 | tail -20" - | - | Hook response - | Hook wraps result in agent-specific JSON, returns to LLM agent - | - v -LLM Agent executes rewritten command - (bash handles && and |, each rtk invocation is a separate process) -``` - -Key design decisions: -- **Lexer-based tokenization**: A single-pass state machine (`lexer.rs`) handles all shell constructs (quotes, escapes, redirects, operators). Used for both compound splitting and redirect stripping. -- **Segment-level rewriting**: Compound commands are split by operators, each segment rewritten independently. Bash recombines them at execution time. -- **Pipe semantics**: Only the left side of `|` is rewritten. The pipe consumer (grep, head, wc) runs raw. `find`/`fd` before a pipe is never rewritten (output format incompatible with xargs). -- **Double env prefix handling**: `classify_command()` strips env prefixes to match the underlying command against rules. `rewrite_segment()` extracts the same prefix separately to re-prepend it to the rewritten command. -- **Fallback contract**: If any segment fails to match, it stays raw. `rewrite_command()` returns `None` only when zero segments were rewritten. - -### 3.3 CLI Parsing and Routing - -Once the rewritten command reaches RTK: - -1. **Telemetry**: `telemetry::maybe_ping()` fires a non-blocking daily usage ping -2. **Clap parsing**: `Cli::try_parse()` matches against the `Commands` enum -3. **Hook check**: `hook_check::maybe_warn()` warns if the installed hook is outdated (rate-limited to 1/day) -4. **Integrity check**: `integrity::runtime_check()` verifies the hook's SHA-256 hash for operational commands -5. **Routing**: A `match cli.command` dispatches to the specialized filter module - -If Clap parsing fails (command not in the enum), the fallback path runs instead. - -### 3.4 Filter Execution - -RTK has two filter systems: - -**Rust Filters**: Compiled modules in `src/cmds/` that execute the command, parse its output, and apply specialized transformations (regex, JSON, state machines). - -**TOML DSL Filters**: Declarative filters in `src/filters/*.toml` that apply regex-based line filtering, truncation, and section extraction. Applied in `run_fallback()` when no Rust filter matches. - -Each filter module follows the same pattern: -1. Start a timer (`TimedExecution::start()`) -2. Execute the underlying command (`std::process::Command`) -3. Apply filtering (strip boilerplate, group errors, truncate) -4. On filter error, fall back to raw output -5. Track token savings to SQLite -6. Propagate exit code - -> **Details**: [`src/cmds/README.md`](../src/cmds/README.md) covers the common pattern, ecosystem organization, cross-command dependencies, and how to add new filters. - -### 3.5 Fallback Path - -When Clap parsing fails (unknown command): - -1. Guard: check if the command is an RTK meta-command (`gain`, `init`, etc.) -- if so, show Clap error -2. Look up TOML DSL filters via `toml_filter::find_matching_filter()` -3. If TOML match: capture stdout, apply filter pipeline, track savings -4. If no match: pure passthrough with `Stdio::inherit`, track as 0% savings - -``` -Command received - -> Clap parse succeeds? - -> Yes: Route to Rust filter module - -> No: run_fallback() - -> TOML filter match? - -> Yes: Capture stdout, apply filter, track savings - -> No: Passthrough (inherit stdio, track 0% savings) -``` - -> **Details**: [`src/core/README.md`](../src/core/README.md) covers the TOML filter engine, filter pipeline stages, and trust-gated project filters. - -### 3.6 Token Tracking - -Every command execution records metrics to SQLite (`~/.local/share/rtk/tracking.db`): - -- Input tokens (raw output size) and output tokens (filtered size) -- Savings percentage, execution time, project path -- 90-day automatic retention cleanup -- Token estimation: `ceil(chars / 4.0)` approximation - -Analytics commands (`rtk gain`, `rtk cc-economics`, `rtk session`) query this database to produce dashboards and ROI reports. - -> **Details**: [`src/analytics/README.md`](../src/analytics/README.md) covers the analytics modules, and [`src/core/README.md`](../src/core/README.md) covers the tracking database schema. - -### 3.7 Tee Recovery - -On command failure (non-zero exit code): - -1. Raw unfiltered output is saved to `~/.local/share/rtk/tee/{epoch}_{slug}.log` -2. A hint line is printed: `[full output: ~/.../tee/1234_cargo_test.log]` -3. LLM agents can re-read the file instead of re-running the failed command - -Tee is configurable (enabled/disabled, min size, max files, max file size) and never affects command output or exit code on failure. - -> **Details**: [`src/core/README.md`](../src/core/README.md) covers tee configuration and the rotation strategy. - ---- - -## 4. Folder Map - -Start here, then drill down into each README for file-level details. - -### `src/` — Rust source code - -| Directory | What it does | What you'll find in its README | -|-----------|-------------|-------------------------------| -| `main.rs` | CLI entry point, `Commands` enum, routing match | _(no README — read the file directly)_ | -| [`core/`](../src/core/README.md) | Shared infrastructure | Tracking DB schema, config system, tee recovery, TOML filter engine, utility functions | -| [`hooks/`](../src/hooks/README.md) | Hook system | Installation flow (`rtk init`), integrity verification, rewrite command, trust model | -| [`analytics/`](../src/analytics/README.md) | Token savings analytics | `rtk gain` dashboard, Claude Code economics, ccusage parsing | -| [`cmds/`](../src/cmds/README.md) | **Command filters (9 ecosystems)** | Common filter pattern, cross-command routing, token savings table, **links to each ecosystem** | -| [`discover/`](../src/discover/README.md) | History analysis + rewrite registry | Rewrite patterns, session providers, compound command splitting | -| [`learn/`](../src/learn/README.md) | CLI correction detection | Error classification, correction pair detection, rule generation | -| [`parser/`](../src/parser/README.md) | Parser infrastructure | Canonical types (TestResult, LintResult, etc.), 3-tier format modes, migration guide | -| [`filters/`](../src/filters/README.md) | TOML filter configs | TOML DSL syntax, 8-stage pipeline, inline testing, naming conventions | - -### `hooks/` — Deployed hook artifacts (root directory) - -| Directory | Agent | What you'll find in its README | -|-----------|-------|-------------------------------| -| [`hooks/`](../hooks/README.md) | _(parent)_ | **All JSON formats**, rewrite registry overview, exit code contract, override controls | -| [`claude/`](../hooks/claude/README.md) | Claude Code | Shell hook mechanism, `PreToolUse` JSON, test script | -| [`copilot/`](../hooks/copilot/README.md) | GitHub Copilot | Rust binary hook, VS Code Chat vs Copilot CLI dual format | -| [`cursor/`](../hooks/cursor/README.md) | Cursor IDE | Shell hook, empty JSON response requirement | -| [`cline/`](../hooks/cline/README.md) | Cline / Roo Code | Rules file (prompt-level, no programmatic hook) | -| [`windsurf/`](../hooks/windsurf/README.md) | Windsurf / Cascade | Rules file (workspace-scoped) | -| [`codex/`](../hooks/codex/README.md) | OpenAI Codex CLI | Awareness document, AGENTS.md integration | -| [`opencode/`](../hooks/opencode/README.md) | OpenCode | TypeScript plugin, zx library, in-place mutation | - ---- - -## 5. Hook System Summary - -RTK supports the following LLM agents through hook integrations: - -| Agent | Hook Type | Mechanism | Can Modify Command? | -|-------|-----------|-----------|---------------------| -| Claude Code | Shell hook | `PreToolUse` in `settings.json` | Yes (`updatedInput`) | -| GitHub Copilot (VS Code) | Rust binary | `rtk hook copilot` reads JSON | Yes (`updatedInput`) | -| GitHub Copilot CLI | Rust binary | `rtk hook copilot` reads JSON | No (deny + suggestion) | -| Cursor | Shell hook | `preToolUse` hook | Yes (`updated_input`) | -| Gemini CLI | Rust binary | `rtk hook gemini` reads JSON | Yes (`hookSpecificOutput`) | -| Cline/Roo Code | Rules file | Prompt-level guidance | N/A (prompt) | -| Windsurf | Rules file | Prompt-level guidance | N/A (prompt) | -| Codex CLI | Awareness doc | AGENTS.md integration | N/A (prompt) | -| OpenCode | TS plugin | `tool.execute.before` event | Yes (in-place mutation) | - -> **Details**: [`hooks/README.md`](../hooks/README.md) has the full JSON schemas for each agent. [`src/hooks/README.md`](../src/hooks/README.md) covers installation, integrity verification, and the rewrite command. - ---- - -## 6. Filter Pipeline Summary - -### Rust Filters (cmds/**) - -Compiled filter modules for complex transformations with 60-95% token savings. - -> **Details**: [`src/cmds/README.md`](../src/cmds/README.md) and each ecosystem subdirectory README. - -### TOML DSL Filters (src/filters/*.toml) - -Declarative filters with an 8-stage pipeline: strip ANSI, regex replace, match output, strip/keep lines, truncate lines, head/tail, max lines, on-empty message. Loaded from three tiers: built-in (compiled), global (`~/.config/rtk/filters/`), project-local (`.rtk/filters/`, trust-gated). - -> **Details**: [`src/core/README.md`](../src/core/README.md) covers the TOML filter engine. - ---- - -## 7. Performance Constraints - -| Metric | Target | Verification | -|--------|--------|--------------| -| Startup time | < 10ms | `hyperfine 'rtk git status' 'git status'` | -| Memory usage | < 5MB resident | `/usr/bin/time -v rtk git status` | -| Binary size | < 5MB stripped | `ls -lh target/release/rtk` | -| Token savings | 60-90% per filter | Snapshot + token count tests | - -Achieved through: -- Zero async overhead (single-threaded, no tokio) -- Lazy regex compilation (`lazy_static!`) -- Minimal allocations (borrow over clone) -- No config file I/O on startup (loaded on-demand) - ---- - -## 8. Testing - -Tests live **in the module file itself** inside a `#[cfg(test)] mod tests` block (e.g., tests for `src/cmds/cloud/container.rs` go at the bottom of that same file). - -### How to Write Tests - -**1. Create a fixture from real command output** (not synthetic data): -```bash -kubectl get pods > tests/fixtures/kubectl_pods_raw.txt -``` - -**2. Write your test in the same module file** (`#[cfg(test)] mod tests`): -```rust -#[test] -fn test_my_filter() { - let input = include_str!("../tests/fixtures/my_cmd_raw.txt"); - let output = filter_my_cmd(input); - assert!(output.contains("expected content")); - assert!(!output.contains("noise line")); -} -``` - -**3. Verify token savings** (60% minimum required): -```rust -#[test] -fn test_my_filter_savings() { - let input = include_str!("../tests/fixtures/my_cmd_raw.txt"); - let output = filter_my_cmd(input); - let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0); - assert!(savings >= 60.0, "Expected >=60% savings, got {:.1}%", savings); -} -``` - -### Test Organization - -``` -tests/ -├── fixtures/ # Real command output (never synthetic) -│ ├── git_log_raw.txt -│ ├── cargo_test_raw.txt -│ └── dotnet/ # Ecosystem-specific fixtures -└── integration_test.rs # Integration tests (#[ignore]) -``` - -- **Unit tests**: `#[cfg(test)] mod tests` embedded in each module -- **Fixtures**: real command output in `tests/fixtures/` -- **Integration tests**: `#[ignore]` attribute, run with `cargo test --ignored` - -> For testing requirements, pre-commit gate, and PR checklist, see [CONTRIBUTING.md — Testing](../CONTRIBUTING.md#testing). - ---- - -## 9. Future Improvements - -- **Extract cli.rs**: Move `Commands` enum, 13 sub-enums (`GitCommands`, `CargoCommands`, etc.), and `AgentTarget` from main.rs to a dedicated cli.rs module. This would reduce main.rs from ~2600 to ~1500 lines. -- **Split routing**: Extract the `match cli.command { ... }` block into a separate routing module. -- **Streaming filters**: For long-running commands, filter output line-by-line as it arrives instead of buffering. +This fork does not include telemetry or any call-home path. Local `rtk gain` stats remain local-only. diff --git a/docs/guide/analytics/discover.md b/docs/guide/analytics/discover.md deleted file mode 100644 index 575ca73b2..000000000 --- a/docs/guide/analytics/discover.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Discover and Session -description: Find missed savings opportunities with rtk discover, and track RTK adoption with rtk session -sidebar: - order: 2 ---- - -# Discover and Session - -## rtk discover — find missed savings - -`rtk discover` analyzes your Claude Code command history to identify commands that ran without RTK filtering and calculates how many tokens you lost. - -```bash -rtk discover # analyze current project history -rtk discover --all # all projects -rtk discover --all --since 7 # last 7 days, all projects -``` - -**Example output:** - -``` -Missed savings analysis (last 7 days) -──────────────────────────────────── -Command Count Est. lost -cargo test 12 ~48,000 tokens -git log 8 ~12,000 tokens -pnpm list 3 ~6,000 tokens -──────────────────────────────────── -Total missed: 23 ~66,000 tokens - -Run `rtk init --global` to capture these automatically. -``` - -If commands appear in the missed list after installing RTK, it usually means the hook isn't active for that agent. See [Troubleshooting](../resources/troubleshooting.md) — "Agent not using RTK". - -## rtk session — adoption tracking - -`rtk session` shows RTK adoption across recent Claude Code sessions: how many shell commands ran through RTK vs. raw. - -```bash -rtk session -``` - -**Example output:** - -``` -Recent sessions (last 10) -───────────────────────────────────────────────────── -Session Total RTK Coverage -2026-04-06 14:32 (45 cmds) 45 43 95.6% -2026-04-05 09:14 (38 cmds) 38 38 100.0% -2026-04-04 16:50 (52 cmds) 52 49 94.2% -───────────────────────────────────────────────────── -Average coverage: 96.6% -``` - -Low coverage on a session usually means RTK was disabled (`RTK_DISABLED=1`) or the hook wasn't active for a specific subagent. diff --git a/docs/guide/analytics/gain.md b/docs/guide/analytics/gain.md deleted file mode 100644 index 706508fce..000000000 --- a/docs/guide/analytics/gain.md +++ /dev/null @@ -1,215 +0,0 @@ ---- -title: Token Savings Analytics -description: Measure and analyze your RTK token savings with rtk gain -sidebar: - order: 1 ---- - -# Token Savings Analytics - -`rtk gain` shows how many tokens RTK has saved across all your commands, with daily, weekly, and monthly breakdowns. - -## Quick reference - -```bash -# Default summary -rtk gain - -# Temporal breakdowns -rtk gain --daily # all days since tracking started -rtk gain --weekly # aggregated by week -rtk gain --monthly # aggregated by month -rtk gain --all # all breakdowns at once - -# Classic flags -rtk gain --graph # ASCII graph, last 30 days -rtk gain --history # last 10 commands -rtk gain --quota # monthly quota savings estimate (default tier: 20x) -rtk gain --quota -t pro # use pro tier token budget for estimate - -# Export -rtk gain --all --format json > savings.json -rtk gain --all --format csv > savings.csv -``` - -## Daily breakdown - -```bash -rtk gain --daily -``` - -``` -📅 Daily Breakdown (3 days) -════════════════════════════════════════════════════════════════ -Date Cmds Input Output Saved Save% -──────────────────────────────────────────────────────────────── -2026-01-28 89 380.9K 26.7K 355.8K 93.4% -2026-01-29 102 894.5K 32.4K 863.7K 96.6% -2026-01-30 5 749 55 694 92.7% -──────────────────────────────────────────────────────────────── -TOTAL 196 1.3M 59.2K 1.2M 95.6% -``` - -- **Cmds**: RTK commands executed -- **Input**: Estimated tokens from raw command output -- **Output**: Actual tokens after filtering -- **Saved**: Input - Output (tokens that never reached the LLM) -- **Save%**: Saved / Input × 100 - -## Weekly and monthly breakdowns - -```bash -rtk gain --weekly -rtk gain --monthly -``` - -Same columns as daily, aggregated by Sunday-Saturday week or calendar month. - -## Export formats - -| Format | Flag | Use case | -|--------|------|----------| -| `text` | default | Terminal display | -| `json` | `--format json` | Programmatic analysis, dashboards | -| `csv` | `--format csv` | Excel, Python/R, Google Sheets | - -**JSON structure:** -```json -{ - "summary": { - "total_commands": 196, - "total_input": 1276098, - "total_output": 59244, - "total_saved": 1220217, - "avg_savings_pct": 95.62 - }, - "daily": [...], - "weekly": [...], - "monthly": [...] -} -``` - -## Typical savings by command - -| Command | Typical savings | Mechanism | -|---------|----------------|-----------| -| `git status` | 77-93% | Compact stat format | -| `eslint` | 84% | Group by rule | -| `jest` | 94-99% | Show failures only | -| `vitest` | 94-99% | Show failures only | -| `find` | 75% | Tree format | -| `pnpm list` | 70-90% | Compact dependencies | -| `grep` | 70% | Truncate + group | - -## How token estimation works - -RTK estimates tokens using `text.len() / 4` (4 characters per token average). This is accurate to ±10% compared to actual LLM tokenization — sufficient for trend analysis. - -``` -Input Tokens = estimate_tokens(raw_command_output) -Output Tokens = estimate_tokens(rtk_filtered_output) -Saved Tokens = Input - Output -Savings % = (Saved / Input) × 100 -``` - -## Database - -Savings data is stored locally in SQLite: - -- **Location**: `~/.local/share/rtk/history.db` (Linux / macOS) -- **Retention**: 90 days (automatic cleanup) -- **Scope**: Global across all projects and Claude sessions - -```bash -# Inspect raw data -sqlite3 ~/.local/share/rtk/history.db \ - "SELECT timestamp, rtk_cmd, saved_tokens FROM commands - ORDER BY timestamp DESC LIMIT 10" - -# Backup -cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db - -# Reset -rm ~/.local/share/rtk/history.db # recreated on next command -``` - -## Analysis workflows - -```bash -# Weekly progress: generate a CSV report every Monday -rtk gain --weekly --format csv > reports/week-$(date +%Y-%W).csv - -# Monthly budget review -rtk gain --monthly --format json | jq '.monthly[] | - {month, saved_tokens, quota_pct: (.saved_tokens / 6000000 * 100)}' - -# Cron: daily JSON snapshot for a dashboard -0 0 * * * rtk gain --all --format json > /var/www/dashboard/rtk-stats.json -``` - -**Python/pandas:** -```python -import pandas as pd -import subprocess - -result = subprocess.run(['rtk', 'gain', '--all', '--format', 'csv'], - capture_output=True, text=True) -lines = result.stdout.split('\n') -daily_start = lines.index('# Daily Data') + 2 -daily_end = lines.index('', daily_start) -daily_df = pd.read_csv(pd.StringIO('\n'.join(lines[daily_start:daily_end]))) -daily_df['date'] = pd.to_datetime(daily_df['date']) -daily_df.plot(x='date', y='savings_pct', kind='line') -``` - -**GitHub Actions (weekly stats):** -```yaml -on: - schedule: - - cron: '0 0 * * 1' -jobs: - stats: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - run: cargo install rtk - - run: rtk gain --weekly --format json > stats/week-$(date +%Y-%W).json - - run: git add stats/ && git commit -m "Weekly rtk stats" && git push -``` - -## Quota estimate - -`--quota` estimates how many tokens RTK has saved relative to your monthly subscription budget, so you can see the cost impact of those savings. - -```bash -rtk gain --quota # uses 20x tier by default -rtk gain --quota -t pro # Claude Pro plan budget -rtk gain --quota -t 5x # 5× usage plan budget -rtk gain --quota -t 20x # 20× usage plan budget -``` - -The tiers (`pro`, `5x`, `20x`) correspond to Anthropic Claude API subscription levels, each with a different monthly token allocation. RTK uses those allocations as a denominator to express your savings as a percentage of your budget. - -:::tip[Find missed savings] -`rtk gain` shows what RTK saved. To find commands that ran *without* RTK and calculate what you lost, see [rtk discover](./discover.md). -::: - -## Troubleshooting - -**No data showing:** -```bash -ls -lh ~/.local/share/rtk/history.db -sqlite3 ~/.local/share/rtk/history.db "SELECT COUNT(*) FROM commands" -git status # run any tracked command to generate data -``` - -**Incorrect statistics:** Token estimation is a heuristic. For precise counts, use `tiktoken`: -```bash -pip install tiktoken -git status > output.txt -python -c " -import tiktoken -enc = tiktoken.get_encoding('cl100k_base') -print(len(enc.encode(open('output.txt').read())), 'actual tokens') -" -``` diff --git a/docs/guide/getting-started/configuration.md b/docs/guide/getting-started/configuration.md deleted file mode 100644 index da7602391..000000000 --- a/docs/guide/getting-started/configuration.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Configuration -description: Customize RTK behavior via config.toml, environment variables, and per-project filters -sidebar: - order: 4 ---- - -# Configuration - -## Config file location - -| Platform | Path | -|----------|------| -| Linux | `~/.config/rtk/config.toml` | -| macOS | `~/Library/Application Support/rtk/config.toml` | - -```bash -rtk config # show current configuration -rtk config --create # create config file with defaults -``` - -## Full config structure - -```toml -[tracking] -enabled = true # enable/disable token tracking -history_days = 90 # retention in days (auto-cleanup) -database_path = "/custom/path/history.db" # optional override - -[display] -colors = true # colored output -emoji = true # use emojis in output -max_width = 120 # maximum output width - -[filters] -# These apply to file-reading commands (ls, find, grep, cat/rtk read). -# Paths matching these patterns are excluded from output, keeping noise low. -ignore_dirs = [".git", "node_modules", "target", "__pycache__", ".venv", "vendor"] -ignore_files = ["*.lock", "*.min.js", "*.min.css"] - -[tee] -enabled = true # save raw output on failure -mode = "failures" # "failures" (default), "always", "never" -max_files = 20 # rotation: keep last N files -# directory = "/custom/tee/path" # optional override - -[telemetry] -enabled = true # anonymous daily ping — see Telemetry & Privacy for full details - -[hooks] -exclude_commands = [] # commands to never auto-rewrite -``` - -For full details on what is collected, opt-out options, and GDPR rights, see [Telemetry & Privacy](../resources/telemetry.md). - -## Environment variables - -| Variable | Description | -|----------|-------------| -| `RTK_DISABLED=1` | Disable RTK for a single command (`RTK_DISABLED=1 git status`) | -| `RTK_TEE_DIR` | Override the tee directory | -| `RTK_TELEMETRY_DISABLED=1` | Disable telemetry | -| `RTK_HOOK_AUDIT=1` | Enable hook audit logging | -| `SKIP_ENV_VALIDATION=1` | Skip env validation (useful with Next.js) | - -## Tee system - -When a command fails, RTK saves the full raw output to a local file and prints the path: - -``` -FAILED: 2/15 tests -[full output: ~/.local/share/rtk/tee/1707753600_cargo_test.log] -``` - -Your AI assistant can then read the file if it needs more detail, without re-running the command. - -| Setting | Default | Description | -|---------|---------|-------------| -| `tee.enabled` | `true` | Enable/disable | -| `tee.mode` | `"failures"` | `"failures"`, `"always"`, `"never"` | -| `tee.max_files` | `20` | Rotation: keep last N files | -| Min size | 500 bytes | Outputs shorter than this are not saved | -| Max file size | 1 MB | Truncated above this | - -## Excluding commands from auto-rewrite - -Prevent specific commands from being rewritten by the hook: - -```toml -[hooks] -exclude_commands = ["git rebase", "git cherry-pick", "docker exec"] -``` - -Patterns match against the full command after stripping env prefixes (`sudo`, `VAR=val`), so `"psql"` excludes both `psql -h localhost` and `PGPASSWORD=x psql -h localhost`. - -Subcommand patterns work too: `"git push"` excludes `git push origin main` but not `git status`. - -Patterns starting with `^` are treated as regex: - -```toml -[hooks] -exclude_commands = ["^curl", "^wget", "git rebase"] -``` - -Invalid regex patterns fall back to prefix matching. - -Or for a single invocation: - -```bash -RTK_DISABLED=1 git rebase main -``` - -## Telemetry - -RTK sends one anonymous ping per day (23h interval). No personal data, no file paths, no command content. - -Data sent: device hash, version, OS, architecture, command count/24h, top commands, savings %. - -To opt out: - -```bash -# Via environment variable -export RTK_TELEMETRY_DISABLED=1 - -# Via config.toml -[telemetry] -enabled = false -``` - -## Per-project filters - -Create `.rtk/filters.toml` in your project root to add custom filters or override built-ins. See [`src/filters/README.md`](https://github.com/rtk-ai/rtk/blob/master/src/filters/README.md) for the full TOML DSL reference. diff --git a/docs/guide/getting-started/installation.md b/docs/guide/getting-started/installation.md deleted file mode 100644 index a07c025b7..000000000 --- a/docs/guide/getting-started/installation.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Installation -description: Install RTK via curl, Homebrew, Cargo, or from source, and verify the correct version -sidebar: - order: 1 ---- - -# Installation - -## Name collision warning - -Two unrelated projects share the name `rtk`. Make sure you install the right one: - -- **Rust Token Killer** (`rtk-ai/rtk`) — this project, a token-saving CLI proxy -- **Rust Type Kit** (`reachingforthejack/rtk`) — a different tool for generating Rust types - -The easiest way to verify you have the correct one: run `rtk gain`. It should display token savings stats. If it returns "command not found", you either have the wrong package or RTK is not installed. - -## Check before installing - -```bash -rtk --version # should print: rtk x.y.z -rtk gain # should show token savings stats -``` - -If both commands work, RTK is already installed. Skip to [Project initialization](#project-initialization). - -## Quick install (Linux and macOS) - -```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh -``` - -## Homebrew (macOS and Linux) - -```bash -brew install rtk-ai/tap/rtk -``` - -## Cargo - -:::caution[Name collision risk] -`cargo install rtk` may install **Rust Type Kit** instead of Rust Token Killer — two unrelated projects share the same crate name. Use the explicit Git URL to guarantee the correct package: -::: - -```bash -cargo install --git https://github.com/rtk-ai/rtk rtk -``` - -## Pre-built binaries (Windows, Linux, macOS) - -Download from [GitHub releases](https://github.com/rtk-ai/rtk/releases): - -- macOS: `rtk-x86_64-apple-darwin.tar.gz` / `rtk-aarch64-apple-darwin.tar.gz` -- Linux: `rtk-x86_64-unknown-linux-musl.tar.gz` / `rtk-aarch64-unknown-linux-gnu.tar.gz` -- Windows: `rtk-x86_64-pc-windows-msvc.zip` - -**Windows users**: Extract the zip and place `rtk.exe` in a directory on your PATH. Run RTK from Command Prompt, PowerShell, or Windows Terminal — do not double-click the `.exe` (it prints usage and exits immediately). For full hook support, use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) instead. - -## Verify installation - -```bash -rtk --version # rtk x.y.z -rtk gain # token savings dashboard -``` - -If `rtk gain` fails but `rtk --version` succeeds, you installed Rust Type Kit by mistake. Uninstall it first: - -```bash -cargo uninstall rtk -``` - -Then reinstall using one of the methods above. - -## Project initialization - -Run once per project to enable the Claude Code hook: - -```bash -rtk init -``` - -For a global install that patches `settings.json` automatically: - -```bash -rtk init --global -``` - -## Uninstall - -```bash -rtk init -g --uninstall # remove hook, RTK.md, and settings.json entry -cargo uninstall rtk # remove binary (if installed via Cargo) -brew uninstall rtk # remove binary (if installed via Homebrew) -``` diff --git a/docs/guide/getting-started/quick-start.md b/docs/guide/getting-started/quick-start.md deleted file mode 100644 index 6e1b7b558..000000000 --- a/docs/guide/getting-started/quick-start.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Quick Start -description: Get RTK running in 5 minutes and see your first token savings -sidebar: - order: 2 ---- - -# Quick Start - -This guide walks you through your first RTK commands after installation. - -## Prerequisites - -RTK is installed and verified: - -```bash -rtk --version # rtk x.y.z -rtk gain # shows token savings dashboard -``` - -If not, see [Installation](./installation.md). - -## Step 1: Initialize for your AI assistant - -```bash -# For Claude Code (global — applies to all projects) -rtk init --global - -# For a single project only -cd /your/project && rtk init -``` - -This installs the hook that automatically rewrites commands. Restart your AI assistant after this step. - -## Step 2: Use your tools normally - -Once the hook is installed, nothing changes in how you work. Your AI assistant runs commands as usual — the hook intercepts them transparently and rewrites them before execution. - -For example, when Claude Code runs `cargo test`, the hook rewrites it to `rtk cargo test` before it executes. The LLM receives filtered output with only the failures — not 500 lines of passing tests. You never see or type `rtk`. - -RTK covers all major ecosystems — Git, Cargo/Rust, JavaScript, Python, Go, Ruby, .NET, Docker/Kubernetes, and more. See [What RTK Optimizes](../resources/what-rtk-covers.md) for the full list. - -## Step 3: Check your savings - -After a few commands, see how much was saved: - -```bash -rtk gain -``` - -``` -Total commands : 12 -Input tokens : 45,230 -Output tokens : 4,890 -Saved : 40,340 (89.2%) -``` - -## Step 4: Unsupported commands - -Commands RTK doesn't recognize run through passthrough — output is unchanged, usage is tracked: - -```bash -rtk proxy make install -``` - -## Next steps - -- [What RTK Optimizes](../resources/what-rtk-covers.md) — all supported commands and savings by ecosystem -- [Supported agents](./supported-agents.md) — Claude Code, Cursor, Copilot, and more -- [Configuration](./configuration.md) — customize RTK behavior diff --git a/docs/guide/index.md b/docs/guide/index.md deleted file mode 100644 index 44e82095d..000000000 --- a/docs/guide/index.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: RTK Documentation -description: RTK (Rust Token Killer) — reduce LLM token consumption by 60-90% on common dev commands, with zero workflow changes -sidebar: - order: 1 ---- - -# RTK — Rust Token Killer - -RTK is a CLI proxy that sits between your AI assistant and your development tools. It filters command output before it reaches the LLM, keeping only what matters and discarding boilerplate, progress bars, and noise. - -**Result:** 60-90% fewer tokens consumed per command, without changing how you work. You run `git status` as usual — RTK's hook intercepts it, filters the output, and the LLM sees a compact 3-line summary instead of 40 lines. - -## How it works - -``` -Your AI assistant runs: git status - ↓ - Hook intercepts (PreToolUse) - ↓ - rtk git status (transparent rewrite) - ↓ - Raw output: 40 lines → Filtered: 3 lines - ~800 tokens → ~60 tokens (92% saved) - ↓ - LLM sees the compact output -``` - -Zero config changes to your workflow. The hook handles everything automatically. - -## What RTK optimizes - -Dozens of commands across all major ecosystems — Git, Cargo/Rust, JavaScript, Python, Go, Ruby, .NET, Docker/Kubernetes, and more. See [What RTK Optimizes](./resources/what-rtk-covers.md) for the full list with savings percentages. - -## Get started - -1. **[Installation](./getting-started/installation.md)** — Install RTK and verify you have the right package -2. **[Quick Start](./getting-started/quick-start.md)** — Connect to your AI assistant in 5 minutes -3. **[Supported Agents](./getting-started/supported-agents.md)** — Claude Code, Cursor, Copilot, Gemini, and more - -## Measure your savings - -```bash -rtk gain # total savings across all sessions -rtk gain --daily # day-by-day breakdown -rtk gain --weekly # weekly aggregation -``` - -See [Token Savings Analytics](./analytics/gain.md) for export formats and analysis workflows. - -## Analyze your usage - -```bash -rtk discover # find commands that ran without RTK (missed savings) -rtk session # RTK adoption rate per Claude Code session -``` - -See [Discover and Session](./analytics/discover.md) for details. - -## Further reading - -- [Configuration](./getting-started/configuration.md) — config.toml, global flags, env vars, tee recovery -- [Troubleshooting](./resources/troubleshooting.md) — common issues and fixes -- [Telemetry & Privacy](./resources/telemetry.md) — what RTK collects and how to opt out -- [ARCHITECTURE.md](https://github.com/rtk-ai/rtk/blob/master/ARCHITECTURE.md) — system design for contributors diff --git a/docs/guide/resources/telemetry.md b/docs/guide/resources/telemetry.md deleted file mode 100644 index b77a0afc7..000000000 --- a/docs/guide/resources/telemetry.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: Telemetry & Privacy -description: What RTK collects, how to opt out, and your GDPR rights -sidebar: - order: 3 ---- - -# Telemetry & Privacy - -RTK collects anonymous, aggregate usage metrics once per day to help improve the product. Telemetry is **disabled by default** and requires explicit consent during `rtk init` or `rtk telemetry enable`. - -## Data Collector - -**Entity**: `RTK AI Labs` -**Contact**: contact@rtk-ai.app - -## Why we collect telemetry - -Without telemetry, we have no visibility into: - -- Which commands are used most and need the best filters -- Which filters are underperforming and need improvement -- Which ecosystems to prioritize for new filter development -- How much value RTK delivers to users (token savings in $ terms) -- Whether users stay engaged over time or churn after trying RTK - -This data directly drives our roadmap. For example, if telemetry shows that 40% of users run Python commands but only 10% of our filters cover Python, we know where to invest next. - -## How it works - -1. **Once per day** (23-hour interval), RTK sends a single HTTPS POST to our telemetry endpoint -2. The ping runs in a **background thread** and never blocks the CLI (2-second timeout) -3. A marker file prevents duplicate pings within the interval -4. If the server is unreachable, the ping is silently dropped — no retries, no queue - -## What is collected - -### Identity (anonymous) - -| Field | Example | Purpose | -|-------|---------|---------| -| `device_hash` | `a3f8c9...` (64 hex chars) | Count unique installations. SHA-256 of a per-device random salt stored locally (`~/.local/share/rtk/.device_salt`). Not reversible. No hostname or username included. | - -### Environment - -| Field | Example | Purpose | -|-------|---------|---------| -| `version` | `0.34.1` | Track adoption of new versions | -| `os` | `macos` | Know which platforms to support and test | -| `arch` | `aarch64` | Prioritize ARM vs x86 builds | -| `install_method` | `homebrew` | Understand distribution channels (homebrew/cargo/script/nix) | - -### Usage volume - -| Field | Example | Purpose | -|-------|---------|---------| -| `commands_24h` | `142` | Daily activity level | -| `commands_total` | `32888` | Lifetime usage — segment light vs heavy users | -| `top_commands` | `["git", "cargo", "ls"]` | Most popular tools (names only, max 5) | -| `tokens_saved_24h` | `450000` | Daily value delivered | -| `tokens_saved_total` | `96500000` | Lifetime value delivered | -| `savings_pct` | `72.5` | Overall effectiveness | - -### Quality (filter improvement) - -| Field | Example | Purpose | -|-------|---------|---------| -| `passthrough_top` | `["git:15", "npm:8"]` | Top 5 commands with 0% savings — these need filters | -| `parse_failures_24h` | `3` | Filter fragility — high count means filters are breaking | -| `low_savings_commands` | `["rtk docker ps:25%"]` | Commands averaging <30% savings — filters to improve | -| `avg_savings_per_command` | `68.5` | Unweighted average (vs global which is volume-biased) | - -### Ecosystem distribution - -| Field | Example | Purpose | -|-------|---------|---------| -| `ecosystem_mix` | `{"git": 45, "cargo": 20, "js": 15}` | Category percentages — where to invest filter development | - -### Retention (engagement) - -| Field | Example | Purpose | -|-------|---------|---------| -| `first_seen_days` | `45` | Installation age in days | -| `active_days_30d` | `22` | Days with at least 1 command in last 30 days — measures stickiness | - -### Economics - -| Field | Example | Purpose | -|-------|---------|---------| -| `tokens_saved_30d` | `12000000` | 30-day token savings for trend analysis | -| `estimated_savings_usd_30d` | `36.0` | Estimated dollar value saved (at ~$3/Mtok input pricing, Claude Sonnet) | - -### Adoption - -| Field | Example | Purpose | -|-------|---------|---------| -| `hook_type` | `claude` | Which AI agent hook is installed (claude/gemini/codex/cursor/none) | -| `custom_toml_filters` | `3` | Number of user-created TOML filter files — DSL adoption | - -### Configuration (user maturity) - -| Field | Example | Purpose | -|-------|---------|---------| -| `has_config_toml` | `true` | Whether user has customized RTK config | -| `exclude_commands_count` | `2` | Commands excluded from rewriting — high count may indicate frustration | -| `projects_count` | `5` | Distinct project paths — multi-project = power user | - -### Feature adoption - -| Field | Example | Purpose | -|-------|---------|---------| -| `meta_usage` | `{"gain": 5, "discover": 2}` | Which RTK features are actually used | - -## What is NOT collected - -- Source code or file contents -- Full command lines or arguments (only tool names like "git", "cargo") -- File paths or directory structures -- Secrets, API keys, or environment variable values -- Repository names or URLs -- Personally identifiable information -- IP addresses (not stored in telemetry pings; stored temporarily in erasure audit log for accountability, anonymized after 6 months) - -## Consent - -Telemetry requires explicit opt-in consent (GDPR Art. 6, 7). Consent is requested during `rtk init` or via `rtk telemetry enable`. Without consent, no data is sent. - -```bash -rtk telemetry status # Check current consent state -rtk telemetry enable # Give consent (interactive prompt) -rtk telemetry disable # Withdraw consent -rtk telemetry forget # Withdraw consent + delete local data + request server erasure -``` - -Environment variable override (blocks telemetry regardless of consent): -```bash -export RTK_TELEMETRY_DISABLED=1 -``` - -## Retention Policy - -- **Server-side**: telemetry records are retained for a maximum of **12 months**, then automatically purged. -- **Server-side (erasure log)**: IP addresses in the erasure audit log are **anonymized after 6 months** (GDPR — IP is personal data). -- **Client-side**: the local SQLite database (`~/.local/share/rtk/history.db`) retains data for **90 days** by default (configurable via `tracking.history_days` in `config.toml`). Deleted entirely by `rtk telemetry forget`. - -## Your Rights (GDPR) - -Under the EU General Data Protection Regulation, you have the right to: - -- **Access** your data: `rtk telemetry status` shows your device hash; the telemetry payload is fully documented above. -- **Rectification**: since data is anonymous and aggregate, rectification is not applicable. -- **Erasure** (Art. 17): run `rtk telemetry forget` to delete local data and send an erasure request to the server. Alternatively, email contact@rtk-ai.app with your device hash. -- **Restriction of processing**: `rtk telemetry disable` stops all data collection immediately. -- **Portability**: the local SQLite database at `~/.local/share/rtk/history.db` contains all locally stored data. -- **Objection**: `rtk telemetry disable` or `export RTK_TELEMETRY_DISABLED=1`. - -## Erasure Procedure - -1. Run `rtk telemetry forget` — this disables telemetry, deletes your device salt, ping marker, and local tracking database (`history.db`), then sends an erasure request to the server. -2. If the server is unreachable, the CLI prints your full device hash and fallback instructions to email contact@rtk-ai.app for manual erasure. -3. You can also email contact@rtk-ai.app directly to request manual erasure. - -## Data Handling - -- All communications use HTTPS (TLS) -- Data is used exclusively for RTK product improvement -- No data is sold or shared with third parties -- Aggregate statistics may be published (e.g. "70% of RTK users are on macOS") diff --git a/docs/guide/resources/troubleshooting.md b/docs/guide/resources/troubleshooting.md deleted file mode 100644 index 51a6fa3be..000000000 --- a/docs/guide/resources/troubleshooting.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Troubleshooting -description: Common RTK issues and how to fix them -sidebar: - order: 2 ---- - -# Troubleshooting - -## `rtk gain` says "not a rtk command" - -**Symptom:** -```bash -$ rtk gain -rtk: 'gain' is not a rtk command. See 'rtk --help'. -``` - -**Cause:** You installed **Rust Type Kit** (`reachingforthejack/rtk`) instead of **Rust Token Killer** (`rtk-ai/rtk`). They share the same binary name. - -**Fix:** -```bash -cargo uninstall rtk -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh -rtk gain # should now show token savings stats -``` - -## How to tell which rtk you have - -| If `rtk gain`... | You have | -|------------------|----------| -| Shows token savings dashboard | Rust Token Killer ✅ | -| Returns "not a rtk command" | Rust Type Kit ❌ | - -## AI assistant not using RTK - -**Symptom:** Claude Code (or another agent) runs `cargo test` instead of `rtk cargo test`. - -**Checklist:** - -1. Verify RTK is installed: - ```bash - rtk --version - rtk gain - ``` - -2. Initialize the hook: - ```bash - rtk init --global # Claude Code - rtk init --global --cursor # Cursor - rtk init --global --opencode # OpenCode - ``` - -3. Restart your AI assistant. - -4. Verify hook status: - ```bash - rtk init --show - ``` - -5. Check `settings.json` has the hook registered (Claude Code): - ```bash - cat ~/.claude/settings.json | grep rtk - ``` - -## RTK not found after `cargo install` - -**Symptom:** -```bash -$ rtk --version -zsh: command not found: rtk -``` - -**Cause:** `~/.cargo/bin` is not in your PATH. - -**Fix:** - -For bash (`~/.bashrc`) or zsh (`~/.zshrc`): -```bash -export PATH="$HOME/.cargo/bin:$PATH" -``` - -For fish (`~/.config/fish/config.fish`): -```fish -set -gx PATH $HOME/.cargo/bin $PATH -``` - -Then reload: -```bash -source ~/.zshrc # or ~/.bashrc -rtk --version -``` - -## RTK on Windows - -### Double-clicking rtk.exe does nothing - -**Symptom:** You double-click `rtk.exe`, a terminal flashes and closes instantly. - -**Cause:** RTK is a command-line tool. With no arguments, it prints usage and exits. The console window opens and closes before you can read anything. - -**Fix:** Open a terminal first, then run RTK from there: -- Press `Win+R`, type `cmd`, press Enter -- Or open PowerShell or Windows Terminal -- Then run: `rtk --version` - -### Hook not working (no auto-rewrite) - -**Symptom:** `rtk init -g` shows "Falling back to --claude-md mode" on Windows. - -**Cause:** The auto-rewrite hook (`rtk-rewrite.sh`) requires a Unix shell. Native Windows doesn't have one. - -**Fix:** Use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) for full hook support: -```bash -# Inside WSL -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -rtk init -g # full hook mode works in WSL -``` - -On native Windows, RTK falls back to CLAUDE.md injection. Your AI assistant gets RTK instructions but won't auto-rewrite commands. It can still use RTK manually: `rtk cargo test`, `rtk git status`, etc. - -### Node.js tools not found - -**Symptom:** -``` -rtk vitest --run -Error: program not found -``` - -**Cause:** On Windows, Node.js tools are installed as `.CMD`/`.BAT` wrappers. Older RTK versions couldn't find them. - -**Fix:** Update to RTK v0.23.1+: -```bash -cargo install --git https://github.com/rtk-ai/rtk -rtk --version # should be 0.23.1+ -``` - -## Compilation error during installation - -```bash -rustup update stable -rustup default stable -cargo clean -cargo build --release -cargo install --path . --force -``` - -Minimum required Rust version: 1.70+. - -## OpenCode not using RTK - -```bash -rtk init --global --opencode -# restart OpenCode -rtk init --show # should show "OpenCode: plugin installed" -``` - -## `cargo install rtk` installs the wrong package - -If Rust Type Kit is published to crates.io under the name `rtk`, `cargo install rtk` may install the wrong one. - -Always use the explicit URL: - -```bash -cargo install --git https://github.com/rtk-ai/rtk -``` - -## Run the diagnostic script - -From the RTK repository root: - -```bash -bash scripts/check-installation.sh -``` - -Checks: -- RTK installed and in PATH -- Correct version (Token Killer, not Type Kit) -- Available features -- Claude Code integration -- Hook status - -## Still stuck? - -Open an issue: https://github.com/rtk-ai/rtk/issues diff --git a/docs/guide/resources/what-rtk-covers.md b/docs/guide/resources/what-rtk-covers.md index dd5c39e89..30b159509 100644 --- a/docs/guide/resources/what-rtk-covers.md +++ b/docs/guide/resources/what-rtk-covers.md @@ -154,4 +154,4 @@ If a command isn't in the list above, RTK runs it through passthrough — the ou rtk proxy make install # runs make install, tracks usage, no filtering ``` -To check which commands were missed opportunities: `rtk discover`. + diff --git a/docs/usage/AUDIT_GUIDE.md b/docs/usage/AUDIT_GUIDE.md deleted file mode 100644 index b641f2450..000000000 --- a/docs/usage/AUDIT_GUIDE.md +++ /dev/null @@ -1,446 +0,0 @@ -# RTK Token Savings Audit Guide - -Complete guide to analyzing your rtk token savings with temporal breakdowns and data exports. - -## Overview - -The `rtk gain` command provides comprehensive analytics for tracking your token savings across time periods. - -**Database Location**: `~/.local/share/rtk/history.db` -**Retention Policy**: 90 days -**Scope**: Global across all projects, worktrees, and Claude sessions - -## Quick Reference - -```bash -# Default summary view -rtk gain - -# Temporal breakdowns -rtk gain --daily # All days since tracking started -rtk gain --weekly # Aggregated by week -rtk gain --monthly # Aggregated by month -rtk gain --all # Show all breakdowns at once - -# Export formats -rtk gain --all --format json > savings.json -rtk gain --all --format csv > savings.csv - -# Combined flags -rtk gain --graph --history --quota # Classic view with extras -rtk gain --daily --weekly --monthly # Multiple breakdowns - -# Reset all tracking data -rtk gain --reset # prompts [y/N] before deleting -rtk gain --reset --yes # skip prompt (CI/scripts) -``` - -## Command Options - -### Temporal Flags - -| Flag | Description | Output | -|------|-------------|--------| -| `--daily` | Day-by-day breakdown | All days with full metrics | -| `--weekly` | Week-by-week breakdown | Aggregated by Sunday-Saturday weeks | -| `--monthly` | Month-by-month breakdown | Aggregated by calendar month | -| `--all` | All time breakdowns | Daily + Weekly + Monthly combined | - -### Classic Flags (still available) - -| Flag | Description | -|------|-------------| -| `--graph` | ASCII graph of last 30 days | -| `--history` | Recent 10 commands | -| `--quota` | Monthly quota analysis (Pro/5x/20x tiers) | -| `--tier ` | Quota tier: pro, 5x, 20x (default: 20x) | - -### Reset Flag - -| Flag | Description | -|------|-------------| -| `--reset` | Permanently delete all tracking data (commands + parse failures) | -| `--yes` | Skip the confirmation prompt (for CI/scripts) | - -> **Warning**: `--reset` is irreversible. It clears both the `commands` and `parse_failures` tables atomically. A `[y/N]` confirmation prompt is shown by default. In non-interactive environments (piped stdin), it defaults to `N` unless `--yes` is passed. - -### Export Formats - -| Format | Flag | Use Case | -|--------|------|----------| -| `text` | `--format text` (default) | Terminal display | -| `json` | `--format json` | Programmatic analysis, APIs | -| `csv` | `--format csv` | Excel, data analysis, plotting | - -## Output Examples - -### Daily Breakdown - -``` -📅 Daily Breakdown (3 days) -════════════════════════════════════════════════════════════════ -Date Cmds Input Output Saved Save% -──────────────────────────────────────────────────────────────── -2026-01-28 89 380.9K 26.7K 355.8K 93.4% -2026-01-29 102 894.5K 32.4K 863.7K 96.6% -2026-01-30 5 749 55 694 92.7% -──────────────────────────────────────────────────────────────── -TOTAL 196 1.3M 59.2K 1.2M 95.6% -``` - -**Metrics explained:** -- **Cmds**: Number of rtk commands executed -- **Input**: Estimated tokens from raw command output -- **Output**: Actual tokens after rtk filtering -- **Saved**: Input - Output (tokens prevented from reaching LLM) -- **Save%**: Percentage reduction (Saved / Input × 100) - -### Weekly Breakdown - -``` -📊 Weekly Breakdown (1 weeks) -════════════════════════════════════════════════════════════════════════ -Week Cmds Input Output Saved Save% -──────────────────────────────────────────────────────────────────────── -01-26 → 02-01 196 1.3M 59.2K 1.2M 95.6% -──────────────────────────────────────────────────────────────────────── -TOTAL 196 1.3M 59.2K 1.2M 95.6% -``` - -**Week definition**: Sunday to Saturday (ISO week starting Sunday at 00:00) - -### Monthly Breakdown - -``` -📆 Monthly Breakdown (1 months) -════════════════════════════════════════════════════════════════ -Month Cmds Input Output Saved Save% -──────────────────────────────────────────────────────────────── -2026-01 196 1.3M 59.2K 1.2M 95.6% -──────────────────────────────────────────────────────────────── -TOTAL 196 1.3M 59.2K 1.2M 95.6% -``` - -**Month format**: YYYY-MM (calendar month) - -### JSON Export - -```json -{ - "summary": { - "total_commands": 196, - "total_input": 1276098, - "total_output": 59244, - "total_saved": 1220217, - "avg_savings_pct": 95.62 - }, - "daily": [ - { - "date": "2026-01-28", - "commands": 89, - "input_tokens": 380894, - "output_tokens": 26744, - "saved_tokens": 355779, - "savings_pct": 93.41 - } - ], - "weekly": [...], - "monthly": [...] -} -``` - -**Use cases:** -- API integration -- Custom dashboards -- Automated reporting -- Data pipeline ingestion - -### CSV Export - -```csv -# Daily Data -date,commands,input_tokens,output_tokens,saved_tokens,savings_pct -2026-01-28,89,380894,26744,355779,93.41 -2026-01-29,102,894455,32445,863744,96.57 - -# Weekly Data -week_start,week_end,commands,input_tokens,output_tokens,saved_tokens,savings_pct -2026-01-26,2026-02-01,196,1276098,59244,1220217,95.62 - -# Monthly Data -month,commands,input_tokens,output_tokens,saved_tokens,savings_pct -2026-01,196,1276098,59244,1220217,95.62 -``` - -**Use cases:** -- Excel analysis -- Python/R data science -- Google Sheets dashboards -- Matplotlib/seaborn plotting - -## Analysis Workflows - -### Weekly Progress Tracking - -```bash -# Generate weekly report every Monday -rtk gain --weekly --format csv > reports/week-$(date +%Y-%W).csv - -# Compare this week vs last week -rtk gain --weekly | tail -3 -``` - -### Monthly Cost Analysis - -```bash -# Export monthly data for budget review -rtk gain --monthly --format json | jq '.monthly[] | - {month, saved_tokens, quota_pct: (.saved_tokens / 6000000 * 100)}' -``` - -### Data Science Analysis - -```python -import pandas as pd -import subprocess - -# Get CSV data -result = subprocess.run(['rtk', 'gain', '--all', '--format', 'csv'], - capture_output=True, text=True) - -# Parse daily data -lines = result.stdout.split('\n') -daily_start = lines.index('# Daily Data') + 2 -daily_end = lines.index('', daily_start) -daily_df = pd.read_csv(pd.StringIO('\n'.join(lines[daily_start:daily_end]))) - -# Plot savings trend -daily_df['date'] = pd.to_datetime(daily_df['date']) -daily_df.plot(x='date', y='savings_pct', kind='line') -``` - -### Excel Analysis - -1. Export CSV: `rtk gain --all --format csv > rtk-data.csv` -2. Open in Excel -3. Create pivot tables: - - Daily trends (line chart) - - Weekly totals (bar chart) - - Savings % distribution (histogram) - -### Dashboard Creation - -```bash -# Generate dashboard data daily via cron -0 0 * * * rtk gain --all --format json > /var/www/dashboard/rtk-stats.json - -# Serve with static site -cat > index.html <<'EOF' - - - -EOF -``` - -## Understanding Token Savings - -### Token Estimation - -rtk estimates tokens using `text.len() / 4` (4 characters per token average). - -**Accuracy**: ±10% compared to actual LLM tokenization (sufficient for trends). - -### Savings Calculation - -``` -Input Tokens = estimate_tokens(raw_command_output) -Output Tokens = estimate_tokens(rtk_filtered_output) -Saved Tokens = Input - Output -Savings % = (Saved / Input) × 100 -``` - -### Typical Savings by Command - -| Command | Typical Savings | Mechanism | -|---------|----------------|-----------| -| `rtk git status` | 77-93% | Compact stat format | -| `rtk eslint` | 84% | Group by rule | -| `rtk jest` | 94-99% | Show failures only | -| `rtk vitest` | 94-99% | Show failures only | -| `rtk find` | 75% | Tree format | -| `rtk pnpm list` | 70-90% | Compact dependencies | -| `rtk grep` | 70% | Truncate + group | - -## Database Management - -### Inspect Raw Data - -```bash -# Location -ls -lh ~/.local/share/rtk/history.db - -# Schema -sqlite3 ~/.local/share/rtk/history.db ".schema" - -# Recent records -sqlite3 ~/.local/share/rtk/history.db \ - "SELECT timestamp, rtk_cmd, saved_tokens FROM commands - ORDER BY timestamp DESC LIMIT 10" - -# Total database size -sqlite3 ~/.local/share/rtk/history.db \ - "SELECT COUNT(*), - SUM(saved_tokens) as total_saved, - MIN(DATE(timestamp)) as first_record, - MAX(DATE(timestamp)) as last_record - FROM commands" -``` - -### Backup & Restore - -```bash -# Backup -cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db - -# Restore -cp ~/backups/rtk-history-20260128.db ~/.local/share/rtk/history.db - -# Export for migration -sqlite3 ~/.local/share/rtk/history.db .dump > rtk-backup.sql -``` - -### Cleanup - -```bash -# Manual cleanup (older than 90 days) -sqlite3 ~/.local/share/rtk/history.db \ - "DELETE FROM commands WHERE timestamp < datetime('now', '-90 days')" - -# Reset all data -rm ~/.local/share/rtk/history.db -# Next rtk command will recreate database -``` - -## Integration Examples - -### GitHub Actions CI/CD - -```yaml -# .github/workflows/rtk-stats.yml -name: RTK Stats Report -on: - schedule: - - cron: '0 0 * * 1' # Weekly on Monday -jobs: - stats: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install rtk - run: cargo install --path . - - name: Generate report - run: | - rtk gain --weekly --format json > stats/week-$(date +%Y-%W).json - - name: Commit stats - run: | - git add stats/ - git commit -m "Weekly rtk stats" - git push -``` - -### Slack Bot - -```python -import subprocess -import json -import requests - -def send_rtk_stats(): - result = subprocess.run(['rtk', 'gain', '--format', 'json'], - capture_output=True, text=True) - data = json.loads(result.stdout) - - message = f""" - 📊 *RTK Token Savings Report* - - Total Saved: {data['summary']['total_saved']:,} tokens - Savings Rate: {data['summary']['avg_savings_pct']:.1f}% - Commands: {data['summary']['total_commands']} - """ - - requests.post(SLACK_WEBHOOK_URL, json={'text': message}) -``` - -## Troubleshooting - -### No data showing - -```bash -# Check if database exists -ls -lh ~/.local/share/rtk/history.db - -# Check record count -sqlite3 ~/.local/share/rtk/history.db "SELECT COUNT(*) FROM commands" - -# Run a tracked command to generate data -rtk git status -``` - -### Export fails - -```bash -# Check for pipe errors -rtk gain --format json 2>&1 | tee /tmp/rtk-debug.log | jq . - -# Use release build to avoid warnings -cargo build --release -./target/release/rtk gain --format json -``` - -### Incorrect statistics - -Token estimation is a heuristic. For precise measurements: - -```bash -# Install tiktoken -pip install tiktoken - -# Validate estimation -rtk git status > output.txt -python -c " -import tiktoken -enc = tiktoken.get_encoding('cl100k_base') -text = open('output.txt').read() -print(f'Actual tokens: {len(enc.encode(text))}') -print(f'rtk estimate: {len(text) // 4}') -" -``` - -## Best Practices - -1. **Regular Exports**: `rtk gain --all --format json > monthly-$(date +%Y%m).json` -2. **Trend Analysis**: Compare week-over-week savings to identify optimization opportunities -3. **Command Profiling**: Use `--history` to see which commands save the most -4. **Backup Before Cleanup**: Always backup before manual database operations -5. **CI Integration**: Track savings across team in shared dashboards - -## See Also - -- [README.md](../README.md) - Full rtk documentation -- [CLAUDE.md](../CLAUDE.md) - Claude Code integration guide -- [ARCHITECTURE.md](../contributing/ARCHITECTURE.md) - Technical architecture diff --git a/docs/usage/FEATURES.md b/docs/usage/FEATURES.md deleted file mode 100644 index ed5ee0bd7..000000000 --- a/docs/usage/FEATURES.md +++ /dev/null @@ -1,1419 +0,0 @@ -# RTK - Documentation fonctionnelle complete - -> **rtk (Rust Token Killer)** -- Proxy CLI haute performance qui reduit la consommation de tokens LLM de 60 a 90%. - -Binaire Rust unique, zero dependances externes, overhead < 10ms par commande. - ---- - -## Table des matieres - -1. [Vue d'ensemble](#vue-densemble) -2. [Drapeaux globaux](#drapeaux-globaux) -3. [Commandes Fichiers](#commandes-fichiers) -4. [Commandes Git](#commandes-git) -5. [Commandes GitHub CLI](#commandes-github-cli) -6. [Commandes Test](#commandes-test) -7. [Commandes Build et Lint](#commandes-build-et-lint) -8. [Commandes Formatage](#commandes-formatage) -9. [Gestionnaires de paquets](#gestionnaires-de-paquets) -10. [Conteneurs et orchestration](#conteneurs-et-orchestration) -11. [Donnees et reseau](#donnees-et-reseau) -12. [Cloud et bases de donnees](#cloud-et-bases-de-donnees) -13. [Stacked PRs (Graphite)](#stacked-prs-graphite) -14. [Analytique et suivi](#analytique-et-suivi) -15. [Systeme de hooks](#systeme-de-hooks) -16. [Configuration](#configuration) -17. [Systeme Tee (recuperation de sortie)](#systeme-tee) -18. [Telemetrie](#telemetrie) - ---- - -## Vue d'ensemble - -rtk agit comme un proxy entre un LLM (Claude Code, Gemini CLI, etc.) et les commandes systeme. Quatre strategies de filtrage sont appliquees selon le type de commande : - -| Strategie | Description | Exemple | -|-----------|-------------|---------| -| **Filtrage intelligent** | Supprime le bruit (commentaires, espaces, boilerplate) | `ls -la` -> arbre compact | -| **Regroupement** | Agregation par repertoire, par type d'erreur, par regle | Tests groupes par fichier | -| **Troncature** | Conserve le contexte pertinent, supprime la redondance | Diff condense | -| **Deduplication** | Fusionne les lignes de log repetees avec compteurs | `error x42` | - -### Mecanisme de fallback - -Si rtk ne reconnait pas une sous-commande, il execute la commande brute (passthrough) et enregistre l'evenement dans la base de suivi. Cela garantit que rtk est **toujours sur** a utiliser -- aucune commande ne sera bloquee. - ---- - -## Drapeaux globaux - -Ces drapeaux s'appliquent a **toutes** les sous-commandes : - -| Drapeau | Court | Description | -|---------|-------|-------------| -| `--verbose` | `-v` | Augmenter la verbosite (-v, -vv, -vvv). Montre les details de filtrage. | -| `--ultra-compact` | `-u` | Mode ultra-compact : icones ASCII, format inline. Economies supplementaires. | -| `--skip-env` | -- | Definit `SKIP_ENV_VALIDATION=1` pour les processus enfants (Next.js, tsc, lint, prisma). | - -**Exemples :** - -```bash -rtk -v git status # Status compact + details de filtrage sur stderr -rtk -vvv cargo test # Verbosite maximale (debug) -rtk -u git log # Log ultra-compact, icones ASCII -rtk --skip-env next build # Desactive la validation d'env de Next.js -``` - ---- - -## Commandes Fichiers - -### `rtk ls` -- Listage de repertoire - -**Objectif :** Remplace `ls` et `tree` avec une sortie optimisee en tokens. - -**Syntaxe :** -```bash -rtk ls [args...] -``` - -Tous les drapeaux natifs de `ls` sont supportes (`-l`, `-a`, `-h`, `-R`, etc.). - -**Economies :** ~80% de reduction de tokens - -**Avant / Apres :** -``` -# ls -la (45 lignes, ~800 tokens) # rtk ls (12 lignes, ~150 tokens) -drwxr-xr-x 15 user staff 480 ... my-project/ --rw-r--r-- 1 user staff 1234 ... +-- src/ (8 files) --rw-r--r-- 1 user staff 567 ... | +-- main.rs -...40 lignes de plus... +-- Cargo.toml - +-- README.md -``` - ---- - -### `rtk tree` -- Arbre de repertoire - -**Objectif :** Proxy vers `tree` natif avec sortie filtree. - -**Syntaxe :** -```bash -rtk tree [args...] -``` - -Supporte tous les drapeaux natifs de `tree` (`-L`, `-d`, `-a`, etc.). - -**Economies :** ~80% - ---- - -### `rtk read` -- Lecture de fichier - -**Objectif :** Remplace `cat`, `head`, `tail` avec un filtrage intelligent du contenu. - -**Syntaxe :** -```bash -rtk read [options] -rtk read - [options] # Lecture depuis stdin -``` - -**Options :** - -| Option | Court | Defaut | Description | -|--------|-------|--------|-------------| -| `--level` | `-l` | `minimal` | Niveau de filtrage : `none`, `minimal`, `aggressive` | -| `--max-lines` | `-m` | illimite | Nombre maximum de lignes | -| `--line-numbers` | `-n` | non | Afficher les numeros de ligne | - -**Niveaux de filtrage :** - -| Niveau | Description | Economies | -|--------|-------------|-----------| -| `none` | Aucun filtrage, sortie brute | 0% | -| `minimal` | Supprime commentaires et lignes vides excessives | ~30% | -| `aggressive` | Signatures uniquement (supprime les corps de fonctions) | ~74% | - -**Avant / Apres (mode aggressive) :** -``` -# cat main.rs (~200 lignes) # rtk read main.rs -l aggressive (~50 lignes) -fn main() -> Result<()> { fn main() -> Result<()> { ... } - let config = Config::load()?; fn process_data(input: &str) -> Vec { ... } - let data = process_data(&input); struct Config { ... } - for item in data { impl Config { fn load() -> Result { ... } } - println!("{}", item); - } - Ok(()) -} -... -``` - -**Langages supportes pour le filtrage :** Rust, Python, JavaScript, TypeScript, Go, C, C++, Java, Ruby, Shell. - ---- - -### `rtk smart` -- Resume heuristique - -**Objectif :** Genere un resume technique de 2 lignes pour un fichier source. - -**Syntaxe :** -```bash -rtk smart [--model heuristic] [--force-download] -``` - -**Economies :** ~95% - -**Exemple :** -``` -$ rtk smart src/tracking.rs -SQLite-based token tracking system for command executions. -Records input/output tokens, savings %, execution times with 90-day retention. -``` - ---- - -### `rtk find` -- Recherche de fichiers - -**Objectif :** Remplace `find` et `fd` avec une sortie compacte groupee par repertoire. - -**Syntaxe :** -```bash -rtk find [args...] -``` - -Supporte a la fois la syntaxe RTK et la syntaxe native `find` (`-name`, `-type`, etc.). - -**Economies :** ~80% - -**Avant / Apres :** -``` -# find . -name "*.rs" (30 lignes) # rtk find "*.rs" . (8 lignes) -./src/main.rs src/ (12 .rs) -./src/git.rs main.rs, git.rs, config.rs -./src/config.rs tracking.rs, filter.rs, utils.rs -./src/tracking.rs ...6 more -./src/filter.rs tests/ (3 .rs) -./src/utils.rs test_git.rs, test_ls.rs, test_filter.rs -...24 lignes de plus... -``` - ---- - -### `rtk grep` -- Recherche dans le contenu - -**Objectif :** Remplace `grep` et `rg` avec une sortie groupee par fichier, tronquee. - -**Syntaxe :** -```bash -rtk grep [chemin] [options] -``` - -**Options :** - -| Option | Court | Defaut | Description | -|--------|-------|--------|-------------| -| `--max-len` | `-l` | 80 | Longueur maximale de ligne | -| `--max` | `-m` | 50 | Nombre maximum de resultats | -| `--context-only` | | non | Afficher uniquement le contexte du match (pas de raccourci, `-c` est reserve a `grep --count`) | -| `--file-type` | `-t` | tous | Filtrer par type (ts, py, rust, etc.) | -| `--line-numbers` | `-n` | oui | Numeros de ligne (toujours actif) | - -Les arguments supplementaires sont transmis a `rg` (ripgrep). Les flags qui changent le format de sortie (`-c`, `-l`, `-L`, `-o`, `-Z`) passent directement a `rg`/`grep` sans filtrage RTK. - -**Economies :** ~80% - -**Avant / Apres :** -``` -# rg "fn run" (20 lignes) # rtk grep "fn run" (10 lignes) -src/git.rs:45:pub fn run(...) src/git.rs -src/git.rs:120:fn run_status(...) 45: pub fn run(...) -src/ls.rs:12:pub fn run(...) 120: fn run_status(...) -src/ls.rs:25:fn run_tree(...) src/ls.rs -... 12: pub fn run(...) - 25: fn run_tree(...) -``` - ---- - -### `rtk diff` -- Diff condense - -**Objectif :** Diff ultra-condense entre deux fichiers (uniquement les lignes modifiees). - -**Syntaxe :** -```bash -rtk diff -rtk diff # Stdin comme second fichier -``` - -**Economies :** ~60% - ---- - -### `rtk wc` -- Comptage compact - -**Objectif :** Remplace `wc` avec une sortie compacte (supprime les chemins et le padding). - -**Syntaxe :** -```bash -rtk wc [args...] -``` - -Supporte tous les drapeaux natifs de `wc` (`-l`, `-w`, `-c`, etc.). - ---- - -## Commandes Git - -### Vue d'ensemble - -Toutes les sous-commandes git sont supportees. Les commandes non reconnues sont transmises directement a git (passthrough). - -**Options globales git :** - -| Option | Description | -|--------|-------------| -| `-C ` | Changer de repertoire avant execution | -| `-c ` | Surcharger une config git | -| `--git-dir ` | Chemin vers le repertoire .git | -| `--work-tree ` | Chemin vers le working tree | -| `--no-pager` | Desactiver le pager | -| `--no-optional-locks` | Ignorer les locks optionnels | -| `--bare` | Traiter comme repo bare | -| `--literal-pathspecs` | Pathspecs literals | - ---- - -### `rtk git status` -- Status compact - -**Economies :** ~80% - -```bash -rtk git status [args...] # Supporte tous les drapeaux git status -``` - -**Avant / Apres :** -``` -# git status (~20 lignes, ~400 tokens) # rtk git status (~5 lignes, ~80 tokens) -On branch main main | 3M 1? 1A -Your branch is up to date with M src/main.rs - 'origin/main'. M src/git.rs - M tests/test_git.rs -Changes not staged for commit: ? new_file.txt - (use "git add ..." to update) A staged_file.rs - modified: src/main.rs - modified: src/git.rs - ... -``` - ---- - -### `rtk git log` -- Historique compact - -**Economies :** ~80% - -```bash -rtk git log [args...] # Supporte --oneline, --graph, --all, -n, etc. -``` - -**Avant / Apres :** -``` -# git log (50+ lignes) # rtk git log -n 5 (5 lignes) -commit abc123def... (HEAD -> main) abc123 Fix token counting bug -Author: User def456 Add vitest support -Date: Mon Jan 15 10:30:00 2024 789abc Refactor filter engine - 012def Update README - Fix token counting bug 345ghi Initial commit -... -``` - ---- - -### `rtk git diff` -- Diff compact - -**Economies :** ~75% - -```bash -rtk git diff [args...] # Supporte --stat, --cached, --staged, etc. -``` - -**Avant / Apres :** -``` -# git diff (~100 lignes) # rtk git diff (~25 lignes) -diff --git a/src/main.rs b/src/main.rs src/main.rs (+5/-2) -index abc123..def456 100644 + let config = Config::load()?; ---- a/src/main.rs + config.validate()?; -+++ b/src/main.rs - // old code -@@ -10,6 +10,8 @@ - let x = 42; - fn main() { src/git.rs (+1/-1) -+ let config = Config::load()?; ~ format!("ok {}", branch) -...30 lignes de headers et contexte... -``` - ---- - -### `rtk git show` -- Show compact - -**Economies :** ~80% - -```bash -rtk git show [args...] -``` - -Affiche le resume du commit + stat + diff compact. - ---- - -### `rtk git add` -- Add ultra-compact - -**Economies :** ~92% - -```bash -rtk git add [args...] # Supporte -A, -p, --all, etc. -``` - -**Sortie :** `ok` (un seul mot) - ---- - -### `rtk git commit` -- Commit ultra-compact - -**Economies :** ~92% - -```bash -rtk git commit -m "message" [args...] # Supporte -a, --amend, --allow-empty, etc. -``` - -**Sortie :** `ok abc1234` (confirmation + hash court) - ---- - -### `rtk git push` -- Push ultra-compact - -**Economies :** ~92% - -```bash -rtk git push [args...] # Supporte -u, remote, branch, etc. -``` - -**Avant / Apres :** -``` -# git push (15 lignes, ~200 tokens) # rtk git push (1 ligne, ~10 tokens) -Enumerating objects: 5, done. ok main -Counting objects: 100% (5/5), done. -Delta compression using up to 8 threads -... -``` - ---- - -### `rtk git pull` -- Pull ultra-compact - -**Economies :** ~92% - -```bash -rtk git pull [args...] -``` - -**Sortie :** `ok 3 files +10 -2` - ---- - -### `rtk git branch` -- Branches compact - -```bash -rtk git branch [args...] # Supporte -d, -D, -m, etc. -``` - -Affiche branche courante, branches locales, branches distantes de facon compacte. - ---- - -### `rtk git fetch` -- Fetch compact - -```bash -rtk git fetch [args...] -``` - -**Sortie :** `ok fetched (N new refs)` - ---- - -### `rtk git stash` -- Stash compact - -```bash -rtk git stash [list|show|pop|apply|drop|push] [args...] -``` - ---- - -### `rtk git worktree` -- Worktree compact - -```bash -rtk git worktree [add|remove|prune|list] [args...] -``` - ---- - -### Passthrough git - -Toute sous-commande git non listee ci-dessus est executee directement : - -```bash -rtk git rebase main # Execute git rebase main -rtk git cherry-pick abc # Execute git cherry-pick abc -rtk git tag v1.0.0 # Execute git tag v1.0.0 -``` - ---- - -## Commandes GitHub CLI - -### `rtk gh` -- GitHub CLI compact - -**Objectif :** Remplace `gh` avec une sortie optimisee. - -**Syntaxe :** -```bash -rtk gh [args...] -``` - -**Sous-commandes supportees :** - -| Commande | Description | Economies | -|----------|-------------|-----------| -| `rtk gh pr list` | Liste des PRs compacte | ~80% | -| `rtk gh pr view ` | Details d'une PR + checks | ~87% | -| `rtk gh pr checks` | Status des checks CI | ~79% | -| `rtk gh issue list` | Liste des issues compacte | ~80% | -| `rtk gh run list` | Status des workflow runs | ~82% | -| `rtk gh api ` | Reponse API compacte | ~26% | - -**Avant / Apres :** -``` -# gh pr list (~30 lignes) # rtk gh pr list (~10 lignes) -Showing 10 of 15 pull requests in org/repo #42 feat: add vitest (open, 2d) - #41 fix: git diff crash (open, 3d) -#42 feat: add vitest support #40 chore: update deps (merged, 5d) - user opened about 2 days ago #39 docs: add guide (merged, 1w) - ... labels: enhancement -... -``` - ---- - -## Commandes Test - -### `rtk test` -- Wrapper de tests generique - -**Objectif :** Execute n'importe quelle commande de test et affiche uniquement les echecs. - -**Syntaxe :** -```bash -rtk test -``` - -**Economies :** ~90% - -**Exemple :** -```bash -rtk test cargo test -rtk test npm test -rtk test bun test -rtk test pytest -``` - -**Avant / Apres :** -``` -# cargo test (200+ lignes en cas d'echec) # rtk test cargo test (~20 lignes) -running 15 tests FAILED: 2/15 tests -test utils::test_parse ... ok test_edge_case: assertion failed -test utils::test_format ... ok test_overflow: panic at utils.rs:18 -test utils::test_edge_case ... FAILED -...150 lignes de backtrace... -``` - ---- - -### `rtk err` -- Erreurs/avertissements uniquement - -**Objectif :** Execute une commande et ne montre que les erreurs et avertissements. - -**Syntaxe :** -```bash -rtk err -``` - -**Economies :** ~80% - -**Exemple :** -```bash -rtk err npm run build -rtk err cargo build -``` - ---- - -### `rtk cargo test` -- Tests Rust - -**Economies :** ~90% - -```bash -rtk cargo test [args...] -``` - -N'affiche que les echecs. Supporte tous les arguments de `cargo test`. - ---- - -### `rtk cargo nextest` -- Tests Rust (nextest) - -```bash -rtk cargo nextest [run|list|--lib] [args...] -``` - -Filtre la sortie de `cargo nextest` pour n'afficher que les echecs. - ---- - -### `rtk jest` / `rtk vitest` -- Tests Jest/Vitest - -**Economies :** ~99.5% - -```bash -rtk jest [args...] -rtk vitest [args...] -``` - ---- - -### `rtk playwright test` -- Tests E2E Playwright - -**Economies :** ~94% - -```bash -rtk playwright [args...] -``` - ---- - -### `rtk pytest` -- Tests Python - -**Economies :** ~90% - -```bash -rtk pytest [args...] -``` - ---- - -### `rtk go test` -- Tests Go - -**Economies :** ~90% - -```bash -rtk go test [args...] -``` - -Utilise le streaming JSON NDJSON de Go pour un filtrage precis. - ---- - -## Commandes Build et Lint - -### `rtk cargo build` -- Build Rust - -**Economies :** ~80% - -```bash -rtk cargo build [args...] -``` - -Supprime les lignes "Compiling...", ne conserve que les erreurs et le resultat final. - ---- - -### `rtk cargo check` -- Check Rust - -**Economies :** ~80% - -```bash -rtk cargo check [args...] -``` - -Supprime les lignes "Checking...", ne conserve que les erreurs. - ---- - -### `rtk cargo clippy` -- Clippy Rust - -**Economies :** ~80% - -```bash -rtk cargo clippy [args...] -``` - -Regroupe les avertissements par regle de lint. - ---- - -### `rtk cargo install` -- Install Rust - -```bash -rtk cargo install [args...] -``` - -Supprime la compilation des dependances, ne conserve que le resultat d'installation et les erreurs. - ---- - -### `rtk tsc` -- TypeScript Compiler - -**Economies :** ~83% - -```bash -rtk tsc [args...] -``` - -Regroupe les erreurs TypeScript par fichier et par code d'erreur. - -**Avant / Apres :** -``` -# tsc --noEmit (50 lignes) # rtk tsc (15 lignes) -src/api.ts(12,5): error TS2345: ... src/api.ts (3 errors) -src/api.ts(15,10): error TS2345: ... TS2345: Argument type mismatch (x2) -src/api.ts(20,3): error TS7006: ... TS7006: Parameter implicitly has 'any' -src/utils.ts(5,1): error TS2304: ... src/utils.ts (1 error) -... TS2304: Cannot find name 'foo' -``` - ---- - -### `rtk lint` -- ESLint / Biome - -**Economies :** ~84% - -```bash -rtk lint [args...] -rtk lint biome [args...] -``` - -Regroupe les violations par regle et par fichier. Auto-detecte le linter. - ---- - -### `rtk prettier` -- Verification du formatage - -**Economies :** ~70% - -```bash -rtk prettier [args...] # ex: rtk prettier --check . -``` - -Affiche uniquement les fichiers necessitant un formatage. - ---- - -### `rtk format` -- Formateur universel - -```bash -rtk format [args...] -``` - -Auto-detecte le formateur du projet (prettier, black, ruff format) et applique un filtre compact. - ---- - -### `rtk next build` -- Build Next.js - -**Economies :** ~87% - -```bash -rtk next [args...] -``` - -Sortie compacte avec metriques de routes. - ---- - -### `rtk ruff` -- Linter/formateur Python - -**Economies :** ~80% - -```bash -rtk ruff check [args...] -rtk ruff format --check [args...] -``` - -Sortie JSON compressee. - ---- - -### `rtk mypy` -- Type checker Python - -```bash -rtk mypy [args...] -``` - -Regroupe les erreurs de type par fichier. - ---- - -### `rtk golangci-lint` -- Linter Go - -**Economies :** ~85% - -```bash -rtk golangci-lint run [args...] -``` - -Sortie JSON compressee. - ---- - -## Commandes Formatage - -### `rtk prettier` -- Prettier - -```bash -rtk prettier --check . -rtk prettier --write src/ -``` - ---- - -### `rtk format` -- Detecteur universel - -```bash -rtk format [args...] -``` - -Detecte automatiquement : prettier, black, ruff format, rustfmt. Applique un filtre compact unifie. - ---- - -## Gestionnaires de paquets - -### `rtk pnpm` -- pnpm - -| Commande | Description | Economies | -|----------|-------------|-----------| -| `rtk pnpm list [-d N]` | Arbre de dependances compact | ~70% | -| `rtk pnpm outdated` | Paquets obsoletes : `pkg: old -> new` | ~80% | -| `rtk pnpm install` | Filtre les barres de progression | ~60% | -| `rtk pnpm build` | Delegue au filtre Next.js | ~87% | -| `rtk pnpm typecheck` | Delegue au filtre tsc | ~83% | - -Les sous-commandes non reconnues sont transmises directement a pnpm (passthrough). - ---- - -### `rtk npm` -- npm - -```bash -rtk npm [args...] # ex: rtk npm run build -``` - -Filtre le boilerplate npm (barres de progression, en-tetes, etc.). - ---- - -### `rtk npx` -- npx avec routage intelligent - -```bash -rtk npx [args...] -``` - -Route intelligemment vers les filtres specialises : -- `rtk npx tsc` -> filtre tsc -- `rtk npx eslint` -> filtre lint -- `rtk npx prisma` -> filtre prisma -- Autres -> passthrough filtre - ---- - -### `rtk pip` -- pip / uv - -```bash -rtk pip list # Liste des paquets (auto-detecte uv) -rtk pip outdated # Paquets obsoletes -rtk pip install # Installation -``` - -Auto-detecte `uv` si disponible et l'utilise a la place de `pip`. - ---- - -### `rtk deps` -- Resume des dependances - -**Objectif :** Resume compact des dependances du projet. - -```bash -rtk deps [chemin] # Defaut: repertoire courant -``` - -Auto-detecte : `Cargo.toml`, `package.json`, `pyproject.toml`, `go.mod`, `Gemfile`, etc. - -**Economies :** ~70% - ---- - -### `rtk prisma` -- ORM Prisma - -| Commande | Description | -|----------|-------------| -| `rtk prisma generate` | Generation du client (supprime l'ASCII art) | -| `rtk prisma migrate dev [--name N]` | Creer et appliquer une migration | -| `rtk prisma migrate status` | Status des migrations | -| `rtk prisma migrate deploy` | Deployer en production | -| `rtk prisma db-push` | Push du schema | - ---- - -## Conteneurs et orchestration - -### `rtk docker` -- Docker - -| Commande | Description | Economies | -|----------|-------------|-----------| -| `rtk docker ps` | Liste compacte des conteneurs | ~80% | -| `rtk docker images` | Liste compacte des images | ~80% | -| `rtk docker logs ` | Logs dedupliques | ~70% | -| `rtk docker compose ps` | Services Compose compacts | ~80% | -| `rtk docker compose logs [service]` | Logs Compose dedupliques | ~70% | -| `rtk docker compose build [service]` | Resume du build | ~60% | - -Les sous-commandes non reconnues sont transmises directement (passthrough). - -**Avant / Apres :** -``` -# docker ps (lignes longues, ~30 tokens/ligne) # rtk docker ps (~10 tokens/ligne) -CONTAINER ID IMAGE COMMAND ... web nginx:1.25 Up 2d (healthy) -abc123def456 nginx:1.25 "/dock..." ... db postgres:16 Up 2d (healthy) -789012345678 postgres:16 "docker..." redis redis:7 Up 1d -``` - ---- - -### `rtk kubectl` -- Kubernetes - -| Commande | Description | Options | -|----------|-------------|---------| -| `rtk kubectl pods [-n ns] [-A]` | Liste compacte des pods | Namespace ou tous | -| `rtk kubectl services [-n ns] [-A]` | Liste compacte des services | Namespace ou tous | -| `rtk kubectl logs [-c container]` | Logs dedupliques | Container specifique | - -Les sous-commandes non reconnues sont transmises directement (passthrough). - ---- - -## Donnees et reseau - -### `rtk json` -- Structure JSON - -**Objectif :** Affiche la structure d'un fichier JSON sans les valeurs. - -```bash -rtk json [--depth N] # Defaut: profondeur 5 -rtk json - # Depuis stdin -``` - -**Economies :** ~60% - -**Avant / Apres :** -``` -# cat package.json (50 lignes) # rtk json package.json (10 lignes) -{ { - "name": "my-app", name: string - "version": "1.0.0", version: string - "dependencies": { dependencies: { 15 keys } - "react": "^18.2.0", devDependencies: { 8 keys } - "next": "^14.0.0", scripts: { 6 keys } - ...15 dependances... } - }, - ... -} -``` - ---- - -### `rtk env` -- Variables d'environnement - -```bash -rtk env # Toutes les variables (sensibles masquees) -rtk env -f AWS # Filtrer par nom -rtk env --show-all # Inclure les valeurs sensibles -``` - -Les variables sensibles (tokens, secrets, mots de passe) sont masquees par defaut : `AWS_SECRET_ACCESS_KEY=***`. - ---- - -### `rtk log` -- Logs dedupliques - -**Objectif :** Filtre et deduplique la sortie de logs. - -```bash -rtk log # Depuis un fichier -rtk log # Depuis stdin (pipe) -``` - -Les lignes repetees sont fusionnees : `[ERROR] Connection refused (x42)`. - -**Economies :** ~60-80% (selon la repetitivite) - ---- - -### `rtk curl` -- HTTP avec troncature - -```bash -rtk curl [args...] -``` - -Tronque les reponses longues et sauvegarde la sortie complete dans un fichier pour recuperation. - ---- - -### `rtk wget` -- Telechargement compact - -```bash -rtk wget [args...] -rtk wget -O - # Sortie vers stdout -``` - -Supprime les barres de progression et le bruit. - ---- - -### `rtk summary` -- Resume heuristique - -**Objectif :** Execute une commande et genere un resume heuristique de la sortie. - -```bash -rtk summary -``` - -Utile pour les commandes longues dont la sortie n'a pas de filtre dedie. - ---- - -### `rtk proxy` -- Passthrough avec suivi - -**Objectif :** Execute une commande **sans filtrage** mais enregistre l'utilisation pour le suivi. - -```bash -rtk proxy -``` - -Utile pour le debug : comparer la sortie brute avec la sortie filtree. - ---- - -## Cloud et bases de donnees - -### `rtk aws` -- AWS CLI - -```bash -rtk aws [args...] -``` - -Force la sortie JSON et compresse le resultat. Supporte tous les services AWS (sts, s3, ec2, ecs, rds, cloudformation, etc.). - ---- - -### `rtk psql` -- PostgreSQL - -```bash -rtk psql [args...] -``` - -Supprime les bordures de tableaux et compresse la sortie. - ---- - -## Stacked PRs (Graphite) - -### `rtk gt` -- Graphite - -| Commande | Description | -|----------|-------------| -| `rtk gt log` | Stack log compact | -| `rtk gt submit` | Submit compact | -| `rtk gt sync` | Sync compact | -| `rtk gt restack` | Restack compact | -| `rtk gt create` | Create compact | -| `rtk gt branch` | Branch info compact | - -Les sous-commandes non reconnues sont transmises directement ou detectees comme passthrough git. - ---- - -## Analytique et suivi - -### Systeme de tracking - -RTK enregistre chaque execution de commande dans une base SQLite : - -- **Emplacement :** `~/.local/share/rtk/tracking.db` (Linux), `~/Library/Application Support/rtk/tracking.db` (macOS) -- **Retention :** 90 jours automatique -- **Metriques :** tokens entree/sortie, pourcentage d'economies, temps d'execution, projet - ---- - -### `rtk gain` -- Statistiques d'economies - -```bash -rtk gain # Resume global -rtk gain -p # Filtre par projet courant -rtk gain --graph # Graphe ASCII (30 derniers jours) -rtk gain --history # Historique recent des commandes -rtk gain --daily # Ventilation jour par jour -rtk gain --weekly # Ventilation par semaine -rtk gain --monthly # Ventilation par mois -rtk gain --all # Toutes les ventilations -rtk gain --quota -t pro # Estimation d'economies sur le quota mensuel -rtk gain --failures # Log des echecs de parsing (commandes en fallback) -rtk gain --format json # Export JSON (pour dashboards) -rtk gain --format csv # Export CSV -``` - -**Options :** - -| Option | Court | Description | -|--------|-------|-------------| -| `--project` | `-p` | Filtrer par repertoire courant | -| `--graph` | `-g` | Graphe ASCII des 30 derniers jours | -| `--history` | `-H` | Historique recent des commandes | -| `--quota` | `-q` | Estimation d'economies sur le quota mensuel | -| `--tier` | `-t` | Tier d'abonnement : `pro`, `5x`, `20x` (defaut: `20x`) | -| `--daily` | `-d` | Ventilation quotidienne | -| `--weekly` | `-w` | Ventilation hebdomadaire | -| `--monthly` | `-m` | Ventilation mensuelle | -| `--all` | `-a` | Toutes les ventilations | -| `--format` | `-f` | Format de sortie : `text`, `json`, `csv` | -| `--failures` | `-F` | Affiche les commandes en fallback | - -**Exemple de sortie :** -``` -$ rtk gain -RTK Token Savings Summary - Total commands: 1,247 - Total input: 2,341,000 tokens - Total output: 468,200 tokens - Total saved: 1,872,800 tokens (80%) - Avg per command: 1,501 tokens saved - -Top commands: - git status 312x -82% - cargo test 156x -91% - git diff 98x -76% -``` - ---- - -### `rtk discover` -- Opportunites manquees - -**Objectif :** Analyse l'historique Claude Code pour trouver les commandes qui auraient pu etre optimisees par rtk. - -```bash -rtk discover # Projet courant, 30 derniers jours -rtk discover --all --since 7 # Tous les projets, 7 derniers jours -rtk discover -p /chemin/projet # Filtrer par projet -rtk discover --limit 20 # Max commandes par section -rtk discover --format json # Export JSON -``` - -**Options :** - -| Option | Court | Description | -|--------|-------|-------------| -| `--project` | `-p` | Filtrer par chemin de projet | -| `--limit` | `-l` | Max commandes par section (defaut: 15) | -| `--all` | `-a` | Scanner tous les projets | -| `--since` | `-s` | Derniers N jours (defaut: 30) | -| `--format` | `-f` | Format : `text`, `json` | - ---- - -### `rtk learn` -- Apprendre des erreurs - -**Objectif :** Analyse l'historique d'erreurs CLI de Claude Code pour detecter les corrections recurrentes. - -```bash -rtk learn # Projet courant -rtk learn --all --since 7 # Tous les projets -rtk learn --write-rules # Generer .claude/rules/cli-corrections.md -rtk learn --min-confidence 0.8 # Seuil de confiance (defaut: 0.6) -rtk learn --min-occurrences 3 # Occurrences minimales (defaut: 1) -rtk learn --format json # Export JSON -``` - ---- - -### `rtk cc-economics` -- Analyse economique Claude Code - -**Objectif :** Compare les depenses Claude Code (via ccusage) avec les economies RTK. - -```bash -rtk cc-economics # Resume -rtk cc-economics --daily # Ventilation quotidienne -rtk cc-economics --weekly # Ventilation hebdomadaire -rtk cc-economics --monthly # Ventilation mensuelle -rtk cc-economics --all # Toutes les ventilations -rtk cc-economics --format json # Export JSON -``` - ---- - -### `rtk hook-audit` -- Metriques du hook - -**Prerequis :** Necessite `RTK_HOOK_AUDIT=1` dans l'environnement. - -```bash -rtk hook-audit # 7 derniers jours (defaut) -rtk hook-audit --since 30 # 30 derniers jours -rtk hook-audit --since 0 # Tout l'historique -``` - ---- - -## Systeme de hooks - -### Fonctionnement - -Le hook RTK intercepte les commandes Bash dans Claude Code **avant leur execution** et les reecrit automatiquement en equivalent RTK. - -**Flux :** -``` -Claude Code "git status" - | - v -settings.json -> PreToolUse hook - | - v -rtk-rewrite.sh (bash) - | - v -rtk rewrite "git status" -> "rtk git status" - | - v -Claude Code execute "rtk git status" - | - v -Sortie filtree retournee a Claude (~10 tokens vs ~200) -``` - -**Points cles :** -- Claude ne voit jamais la recriture -- il recoit simplement une sortie optimisee -- Le hook est un delegateur leger (~50 lignes bash) qui appelle `rtk rewrite` -- Toute la logique de recriture est dans le registre Rust (`src/discover/registry.rs`) -- Les commandes deja prefixees par `rtk` passent sans modification -- Les heredocs (`<<`) ne sont pas modifies -- Les commandes non reconnues passent sans modification - -### Installation - -```bash -rtk init -g # Installation recommandee (hook + RTK.md) -rtk init -g --auto-patch # Non-interactif (CI/CD) -rtk init -g --hook-only # Hook seul, sans RTK.md -rtk init --show # Verifier l'installation -rtk init -g --uninstall # Desinstaller -``` - -### Fichiers installes - -| Fichier | Description | -|---------|-------------| -| `~/.claude/hooks/rtk-rewrite.sh` | Script hook (delegue a `rtk rewrite`) | -| `~/.claude/RTK.md` | Instructions minimales pour le LLM | -| `~/.claude/settings.json` | Enregistrement du hook PreToolUse | - -### `rtk rewrite` -- Recriture de commande - -Commande interne utilisee par le hook. Imprime la commande reecrite sur stdout (exit 0) ou sort avec exit 1 si aucun equivalent RTK n'existe. - -```bash -rtk rewrite "git status" # -> "rtk git status" (exit 0) -rtk rewrite "terraform plan" # -> (exit 1, pas de recriture) -rtk rewrite "rtk git status" # -> "rtk git status" (exit 0, inchange) -``` - -### `rtk verify` -- Verification d'integrite - -Verifie l'integrite du hook installe via un controle SHA-256. - -```bash -rtk verify -``` - -### Commandes reecrites automatiquement - -| Commande brute | Reecrite en | -|----------------|-------------| -| `git status/diff/log/add/commit/push/pull` | `rtk git ...` | -| `gh pr/issue/run` | `rtk gh ...` | -| `cargo test/build/clippy/check` | `rtk cargo ...` | -| `cat/head/tail ` | `rtk read ` | -| `rg/grep ` | `rtk grep ` | -| `ls` | `rtk ls` | -| `tree` | `rtk tree` | -| `wc` | `rtk wc` | -| `jest` | `rtk jest` | -| `vitest` | `rtk vitest` | -| `tsc` | `rtk tsc` | -| `eslint/biome` | `rtk lint` | -| `prettier` | `rtk prettier` | -| `playwright` | `rtk playwright` | -| `prisma` | `rtk prisma` | -| `ruff check/format` | `rtk ruff ...` | -| `pytest` | `rtk pytest` | -| `mypy` | `rtk mypy` | -| `pip list/install` | `rtk pip ...` | -| `go test/build/vet` | `rtk go ...` | -| `golangci-lint` | `rtk golangci-lint` | -| `docker ps/images/logs` | `rtk docker ...` | -| `kubectl get/logs` | `rtk kubectl ...` | -| `curl` | `rtk curl` | -| `pnpm list/outdated` | `rtk pnpm ...` | - -### Exclusion de commandes - -Pour empecher certaines commandes d'etre reecrites, ajoutez-les dans `config.toml` : - -```toml -[hooks] -exclude_commands = ["curl", "playwright"] -``` - ---- - -## Configuration - -### Fichier de configuration - -**Emplacement :** `~/.config/rtk/config.toml` (Linux) ou `~/Library/Application Support/rtk/config.toml` (macOS) - -**Commandes :** -```bash -rtk config # Afficher la configuration actuelle -rtk config --create # Creer le fichier avec les valeurs par defaut -``` - -### Structure complete - -```toml -[tracking] -enabled = true # Activer/desactiver le suivi -history_days = 90 # Jours de retention (nettoyage automatique) -database_path = "/custom/path/tracking.db" # Chemin personnalise (optionnel) - -[display] -colors = true # Sortie coloree -emoji = true # Utiliser les emojis -max_width = 120 # Largeur maximale de sortie - -[filters] -ignore_dirs = [".git", "node_modules", "target", "__pycache__", ".venv", "vendor"] -ignore_files = ["*.lock", "*.min.js", "*.min.css"] - -[tee] -enabled = true # Activer la sauvegarde de sortie brute -mode = "failures" # "failures" (defaut), "always", ou "never" -max_files = 20 # Rotation : garder les N derniers fichiers -# directory = "/custom/tee/path" # Chemin personnalise (optionnel) - -[telemetry] -enabled = false # Telemetrie anonyme (1 ping/jour, requiert consentement) -# consent_given = true # Defini automatiquement par `rtk init` ou `rtk telemetry enable` -# consent_date = "..." # Date du consentement (RFC 3339) - -[hooks] -exclude_commands = [] # Commandes a exclure de la recriture automatique -``` - -### Variables d'environnement - -| Variable | Description | -|----------|-------------| -| `RTK_TEE_DIR` | Surcharge le repertoire tee | -| `RTK_TELEMETRY_DISABLED=1` | Desactiver la telemetrie | -| `RTK_HOOK_AUDIT=1` | Activer l'audit du hook | -| `SKIP_ENV_VALIDATION=1` | Desactiver la validation d'env (Next.js, etc.) | - ---- - -## Systeme Tee - -### Recuperation de sortie brute - -Quand une commande echoue, RTK sauvegarde automatiquement la sortie brute complete dans un fichier log. Cela permet au LLM de lire la sortie sans re-executer la commande. - -**Fonctionnement :** -1. La commande echoue (exit code != 0) -2. RTK sauvegarde la sortie brute dans `~/.local/share/rtk/tee/` -3. Le chemin du fichier est affiche dans la sortie filtree -4. Le LLM peut lire le fichier si besoin de plus de details - -**Sortie :** -``` -FAILED: 2/15 tests -[full output: ~/.local/share/rtk/tee/1707753600_cargo_test.log] -``` - -**Configuration :** - -| Parametre | Defaut | Description | -|-----------|--------|-------------| -| `tee.enabled` | `true` | Activer/desactiver | -| `tee.mode` | `"failures"` | `"failures"`, `"always"`, `"never"` | -| `tee.max_files` | `20` | Rotation : garder les N derniers | -| Taille min | 500 octets | Les sorties trop courtes ne sont pas sauvegardees | -| Taille max fichier | 1 Mo | Troncature au-dela | - ---- - -## Telemetrie - -RTK peut envoyer un ping anonyme une fois par jour (23h d'intervalle) pour des statistiques d'utilisation. La telemetrie est **desactivee par defaut** et requiert un consentement explicite (RGPD Art. 6, 7). - -**Donnees envoyees :** hash de device (SHA-256 d'un sel aleatoire), version, OS, architecture, nombre de commandes/24h, top commandes, pourcentage d'economies. - -**Responsable du traitement :** `RTK AI Labs`, contact@rtk-ai.app - -**Gerer la telemetrie :** -```bash -rtk telemetry status # Voir l'etat du consentement -rtk telemetry enable # Donner son consentement (prompt interactif) -rtk telemetry disable # Retirer son consentement -rtk telemetry forget # Retirer + supprimer donnees locales + demande d'effacement serveur -``` - -**Desactiver via variable d'environnement :** -```bash -export RTK_TELEMETRY_DISABLED=1 -``` - -Aucune donnee personnelle, aucun contenu de commande, aucun chemin de fichier n'est transmis. Conservation serveur : 12 mois max. Details : [docs/TELEMETRY.md](../TELEMETRY.md) - ---- - -## Resume des economies par categorie - -| Categorie | Commandes | Economies typiques | -|-----------|-----------|-------------------| -| **Fichiers** | ls, tree, read, find, grep, diff | 60-80% | -| **Git** | status, log, diff, show, add, commit, push, pull | 75-92% | -| **GitHub** | pr, issue, run, api | 26-87% | -| **Tests** | cargo test, vitest, playwright, pytest, go test | 90-99% | -| **Build/Lint** | cargo build, tsc, eslint, prettier, next, ruff, clippy | 70-87% | -| **Paquets** | pnpm, npm, pip, deps, prisma | 60-80% | -| **Conteneurs** | docker, kubectl | 70-80% | -| **Donnees** | json, env, log, curl, wget | 60-80% | -| **Analytique** | gain, discover, learn, cc-economics | N/A (meta) | - ---- - -## Nombre total de commandes - -RTK supporte **45+ commandes** reparties en 9 categories, avec passthrough automatique pour les sous-commandes non reconnues. Cela en fait un proxy universel : il est toujours sur a utiliser en prefixe. diff --git a/docs/usage/TRACKING.md b/docs/usage/TRACKING.md deleted file mode 100644 index 97ff6d95a..000000000 --- a/docs/usage/TRACKING.md +++ /dev/null @@ -1,583 +0,0 @@ -# RTK Tracking API Documentation - -Comprehensive documentation for RTK's token savings tracking system. - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Public API](#public-api) -- [Usage Examples](#usage-examples) -- [Data Formats](#data-formats) -- [Integration Examples](#integration-examples) -- [Database Schema](#database-schema) - -## Overview - -RTK's tracking system records every command execution to provide analytics on token savings. The system: -- Stores command history in SQLite (~/.local/share/rtk/tracking.db) -- Tracks input/output tokens, savings percentage, and execution time -- Automatically cleans up records older than 90 days -- Provides aggregation APIs (daily/weekly/monthly) -- Exports to JSON/CSV for external integrations - -## Architecture - -### Data Flow - -``` -rtk command execution - ↓ -TimedExecution::start() - ↓ -[command runs] - ↓ -TimedExecution::track(original_cmd, rtk_cmd, input, output) - ↓ -Tracker::record(original_cmd, rtk_cmd, input_tokens, output_tokens, exec_time_ms) - ↓ -SQLite database (~/.local/share/rtk/tracking.db) - ↓ -Aggregation APIs (get_summary, get_all_days, etc.) - ↓ -CLI output (rtk gain) or JSON/CSV export -``` - -### Storage Location - -- **Linux**: `~/.local/share/rtk/tracking.db` -- **macOS**: `~/Library/Application Support/rtk/tracking.db` -- **Windows**: `%APPDATA%\rtk\tracking.db` - -### Data Retention - -Records older than **90 days** are automatically deleted on each write operation to prevent unbounded database growth. - -## Public API - -### Core Types - -#### `Tracker` - -Main tracking interface for recording and querying command history. - -```rust -pub struct Tracker { - conn: Connection, // SQLite connection -} - -impl Tracker { - /// Create new tracker instance (opens/creates database) - pub fn new() -> Result; - - /// Record a command execution - pub fn record( - &self, - original_cmd: &str, // Standard command (e.g., "ls -la") - rtk_cmd: &str, // RTK command (e.g., "rtk ls") - input_tokens: usize, // Estimated input tokens - output_tokens: usize, // Actual output tokens - exec_time_ms: u64, // Execution time in milliseconds - ) -> Result<()>; - - /// Get overall summary statistics - pub fn get_summary(&self) -> Result; - - /// Get daily statistics (all days) - pub fn get_all_days(&self) -> Result>; - - /// Get weekly statistics (grouped by week) - pub fn get_by_week(&self) -> Result>; - - /// Get monthly statistics (grouped by month) - pub fn get_by_month(&self) -> Result>; - - /// Get recent command history (limit = max records) - pub fn get_recent(&self, limit: usize) -> Result>; -} -``` - -#### `GainSummary` - -Aggregated statistics across all recorded commands. - -```rust -pub struct GainSummary { - pub total_commands: usize, // Total commands recorded - pub total_input: usize, // Total input tokens - pub total_output: usize, // Total output tokens - pub total_saved: usize, // Total tokens saved - pub avg_savings_pct: f64, // Average savings percentage - pub total_time_ms: u64, // Total execution time (ms) - pub avg_time_ms: u64, // Average execution time (ms) - pub by_command: Vec<(String, usize, usize, f64, u64)>, // Top 10 commands - pub by_day: Vec<(String, usize)>, // Last 30 days -} -``` - -#### `DayStats` - -Daily statistics (Serializable for JSON export). - -```rust -#[derive(Debug, Serialize)] -pub struct DayStats { - pub date: String, // ISO date (YYYY-MM-DD) - pub commands: usize, // Commands executed this day - pub input_tokens: usize, // Total input tokens - pub output_tokens: usize, // Total output tokens - pub saved_tokens: usize, // Total tokens saved - pub savings_pct: f64, // Savings percentage - pub total_time_ms: u64, // Total execution time (ms) - pub avg_time_ms: u64, // Average execution time (ms) -} -``` - -#### `WeekStats` - -Weekly statistics (Serializable for JSON export). - -```rust -#[derive(Debug, Serialize)] -pub struct WeekStats { - pub week_start: String, // ISO date (YYYY-MM-DD) - pub week_end: String, // ISO date (YYYY-MM-DD) - pub commands: usize, - pub input_tokens: usize, - pub output_tokens: usize, - pub saved_tokens: usize, - pub savings_pct: f64, - pub total_time_ms: u64, - pub avg_time_ms: u64, -} -``` - -#### `MonthStats` - -Monthly statistics (Serializable for JSON export). - -```rust -#[derive(Debug, Serialize)] -pub struct MonthStats { - pub month: String, // YYYY-MM format - pub commands: usize, - pub input_tokens: usize, - pub output_tokens: usize, - pub saved_tokens: usize, - pub savings_pct: f64, - pub total_time_ms: u64, - pub avg_time_ms: u64, -} -``` - -#### `CommandRecord` - -Individual command record from history. - -```rust -pub struct CommandRecord { - pub timestamp: DateTime, // UTC timestamp - pub rtk_cmd: String, // RTK command used - pub saved_tokens: usize, // Tokens saved - pub savings_pct: f64, // Savings percentage -} -``` - -#### `TimedExecution` - -Helper for timing command execution (preferred API). - -```rust -pub struct TimedExecution { - start: Instant, -} - -impl TimedExecution { - /// Start timing a command execution - pub fn start() -> Self; - - /// Track command with elapsed time - pub fn track(&self, original_cmd: &str, rtk_cmd: &str, input: &str, output: &str); - - /// Track passthrough commands (timing-only, no token counting) - pub fn track_passthrough(&self, original_cmd: &str, rtk_cmd: &str); -} -``` - -### Utility Functions - -```rust -/// Estimate token count (~4 chars = 1 token) -pub fn estimate_tokens(text: &str) -> usize; - -/// Format OsString args for display -pub fn args_display(args: &[OsString]) -> String; - -/// Legacy tracking function (deprecated, use TimedExecution) -#[deprecated(note = "Use TimedExecution instead")] -pub fn track(original_cmd: &str, rtk_cmd: &str, input: &str, output: &str); -``` - -## Usage Examples - -### Basic Tracking - -```rust -use rtk::tracking::{TimedExecution, Tracker}; - -fn main() -> anyhow::Result<()> { - // Start timer - let timer = TimedExecution::start(); - - // Execute command - let input = execute_original_command()?; - let output = execute_rtk_command()?; - - // Track execution - timer.track("ls -la", "rtk ls", &input, &output); - - Ok(()) -} -``` - -### Querying Statistics - -```rust -use rtk::tracking::Tracker; - -fn main() -> anyhow::Result<()> { - let tracker = Tracker::new()?; - - // Get overall summary - let summary = tracker.get_summary()?; - println!("Total commands: {}", summary.total_commands); - println!("Total saved: {} tokens", summary.total_saved); - println!("Average savings: {:.1}%", summary.avg_savings_pct); - - // Get daily breakdown - let days = tracker.get_all_days()?; - for day in days.iter().take(7) { - println!("{}: {} commands, {} tokens saved", - day.date, day.commands, day.saved_tokens); - } - - // Get recent history - let recent = tracker.get_recent(10)?; - for cmd in recent { - println!("{}: {} saved {:.1}%", - cmd.timestamp, cmd.rtk_cmd, cmd.savings_pct); - } - - Ok(()) -} -``` - -### Passthrough Commands - -For commands that stream output or run interactively (no output capture): - -```rust -use rtk::tracking::TimedExecution; - -fn main() -> anyhow::Result<()> { - let timer = TimedExecution::start(); - - // Execute streaming command (e.g., git tag --list) - execute_streaming_command()?; - - // Track timing only (input_tokens=0, output_tokens=0) - timer.track_passthrough("git tag --list", "rtk git tag --list"); - - Ok(()) -} -``` - -## Data Formats - -### JSON Export Schema - -#### DayStats JSON - -```json -{ - "date": "2026-02-03", - "commands": 42, - "input_tokens": 15420, - "output_tokens": 3842, - "saved_tokens": 11578, - "savings_pct": 75.08, - "total_time_ms": 8450, - "avg_time_ms": 201 -} -``` - -#### WeekStats JSON - -```json -{ - "week_start": "2026-01-27", - "week_end": "2026-02-02", - "commands": 284, - "input_tokens": 98234, - "output_tokens": 19847, - "saved_tokens": 78387, - "savings_pct": 79.80, - "total_time_ms": 56780, - "avg_time_ms": 200 -} -``` - -#### MonthStats JSON - -```json -{ - "month": "2026-02", - "commands": 1247, - "input_tokens": 456789, - "output_tokens": 91358, - "saved_tokens": 365431, - "savings_pct": 80.00, - "total_time_ms": 249560, - "avg_time_ms": 200 -} -``` - -### CSV Export Schema - -```csv -date,commands,input_tokens,output_tokens,saved_tokens,savings_pct,total_time_ms,avg_time_ms -2026-02-03,42,15420,3842,11578,75.08,8450,201 -2026-02-02,38,14230,3557,10673,75.00,7600,200 -2026-02-01,45,16890,4223,12667,75.00,9000,200 -``` - -## Integration Examples - -### GitHub Actions - Track Savings in CI - -```yaml -# .github/workflows/track-rtk-savings.yml -name: Track RTK Savings - -on: - schedule: - - cron: '0 0 * * 1' # Weekly on Monday - workflow_dispatch: - -jobs: - track-savings: - runs-on: ubuntu-latest - steps: - - name: Install RTK - run: cargo install --git https://github.com/rtk-ai/rtk - - - name: Export weekly stats - run: | - rtk gain --weekly --format json > rtk-weekly.json - cat rtk-weekly.json - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: rtk-metrics - path: rtk-weekly.json - - - name: Post to Slack - if: success() - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - run: | - SAVINGS=$(jq -r '.[0].saved_tokens' rtk-weekly.json) - PCT=$(jq -r '.[0].savings_pct' rtk-weekly.json) - curl -X POST -H 'Content-type: application/json' \ - --data "{\"text\":\"📊 RTK Weekly: ${SAVINGS} tokens saved (${PCT}%)\"}" \ - $SLACK_WEBHOOK -``` - -### Custom Dashboard Script - -```python -#!/usr/bin/env python3 -""" -Export RTK metrics to Grafana/Datadog/etc. -""" -import json -import subprocess -from datetime import datetime - -def get_rtk_metrics(): - """Fetch RTK metrics as JSON.""" - result = subprocess.run( - ["rtk", "gain", "--all", "--format", "json"], - capture_output=True, - text=True - ) - return json.loads(result.stdout) - -def export_to_datadog(metrics): - """Send metrics to Datadog.""" - import datadog - - datadog.initialize(api_key="YOUR_API_KEY") - - for day in metrics.get("daily", []): - datadog.api.Metric.send( - metric="rtk.tokens_saved", - points=[(datetime.now().timestamp(), day["saved_tokens"])], - tags=[f"date:{day['date']}"] - ) - - datadog.api.Metric.send( - metric="rtk.savings_pct", - points=[(datetime.now().timestamp(), day["savings_pct"])], - tags=[f"date:{day['date']}"] - ) - -if __name__ == "__main__": - metrics = get_rtk_metrics() - export_to_datadog(metrics) - print(f"Exported {len(metrics.get('daily', []))} days to Datadog") -``` - -### Rust Integration (Using RTK as Library) - -```rust -// In your Cargo.toml -// [dependencies] -// rtk = { git = "https://github.com/rtk-ai/rtk" } - -use rtk::tracking::{Tracker, TimedExecution}; -use anyhow::Result; - -fn main() -> Result<()> { - // Track your own commands - let timer = TimedExecution::start(); - - let input = run_expensive_operation()?; - let output = run_optimized_operation()?; - - timer.track( - "expensive_operation", - "optimized_operation", - &input, - &output - ); - - // Query aggregated stats - let tracker = Tracker::new()?; - let summary = tracker.get_summary()?; - - println!("Total savings: {} tokens ({:.1}%)", - summary.total_saved, - summary.avg_savings_pct - ); - - // Export to JSON for external tools - let days = tracker.get_all_days()?; - let json = serde_json::to_string_pretty(&days)?; - std::fs::write("metrics.json", json)?; - - Ok(()) -} -``` - -## Database Schema - -### Table: `commands` - -```sql -CREATE TABLE commands ( - id INTEGER PRIMARY KEY, - timestamp TEXT NOT NULL, -- RFC3339 UTC timestamp - original_cmd TEXT NOT NULL, -- Original command (e.g., "ls -la") - rtk_cmd TEXT NOT NULL, -- RTK command (e.g., "rtk ls") - input_tokens INTEGER NOT NULL, -- Estimated input tokens - output_tokens INTEGER NOT NULL, -- Actual output tokens - saved_tokens INTEGER NOT NULL, -- input_tokens - output_tokens - savings_pct REAL NOT NULL, -- (saved/input) * 100 - exec_time_ms INTEGER DEFAULT 0 -- Execution time in milliseconds -); - -CREATE INDEX idx_timestamp ON commands(timestamp); -``` - -### Automatic Cleanup - -On every write operation (`Tracker::record`), records older than 90 days are deleted: - -```rust -fn cleanup_old(&self) -> Result<()> { - let cutoff = Utc::now() - chrono::Duration::days(90); - self.conn.execute( - "DELETE FROM commands WHERE timestamp < ?1", - params![cutoff.to_rfc3339()], - )?; - Ok(()) -} -``` - -### Migration Support - -The system automatically adds new columns if they don't exist (e.g., `exec_time_ms` was added later): - -```rust -// Safe migration on Tracker::new() -let _ = conn.execute( - "ALTER TABLE commands ADD COLUMN exec_time_ms INTEGER DEFAULT 0", - [], -); -``` - -## Performance Considerations - -- **SQLite WAL mode**: Not enabled (may add in future for concurrent writes) -- **Index on timestamp**: Enables fast date-range queries -- **Automatic cleanup**: Prevents database from growing unbounded -- **Token estimation**: ~4 chars = 1 token (simple, fast approximation) -- **Aggregation queries**: Use SQL GROUP BY for efficient aggregation - -## Security & Privacy - -- **Local storage only**: Tracking database never leaves the machine -- **Telemetry requires consent**: RTK can send a daily anonymous usage ping (version, OS, command counts, token savings). Disabled by default, requires explicit consent via `rtk init` or `rtk telemetry enable`. Manage with `rtk telemetry status/disable/forget`. Override: `RTK_TELEMETRY_DISABLED=1` -- **User control**: Users can delete `~/.local/share/rtk/tracking.db` anytime -- **90-day retention**: Old data automatically purged - -## Troubleshooting - -### Database locked error - -If you see "database is locked" errors: -- Ensure only one RTK process writes at a time -- Check file permissions on `~/.local/share/rtk/tracking.db` -- Delete and recreate: `rm ~/.local/share/rtk/tracking.db && rtk gain` - -### Missing exec_time_ms column - -Older databases may not have the `exec_time_ms` column. RTK automatically migrates on first use, but you can force it: - -```bash -sqlite3 ~/.local/share/rtk/tracking.db \ - "ALTER TABLE commands ADD COLUMN exec_time_ms INTEGER DEFAULT 0" -``` - -### Incorrect token counts - -Token estimation uses `~4 chars = 1 token`. This is approximate. For precise counts, integrate with your LLM's tokenizer API. - -## Future Enhancements - -Planned improvements (contributions welcome): - -- [ ] Export to Prometheus/OpenMetrics format -- [ ] Support for custom retention periods (not just 90 days) -- [ ] SQLite WAL mode for concurrent writes -- [ ] Per-project tracking (multiple databases) -- [ ] Integration with Claude API for precise token counts -- [ ] Web dashboard (localhost) for visualizing trends - -## See Also - -- [README.md](../README.md) - Main project documentation -- [COMMAND_AUDIT.md](../claudedocs/COMMAND_AUDIT.md) - List of all RTK commands -- [Rust docs](https://docs.rs/) - Run `cargo doc --open` for API docs diff --git a/hooks/antigravity/rules.md b/hooks/antigravity/rules.md index 8ee44bcd4..af310f0db 100644 --- a/hooks/antigravity/rules.md +++ b/hooks/antigravity/rules.md @@ -21,9 +21,6 @@ rtk gh pr list ## Meta Commands ```bash -rtk gain # Show token savings -rtk gain --history # Command history with savings -rtk discover # Find missed RTK opportunities rtk proxy # Run raw (no filtering, for debugging) ``` diff --git a/hooks/claude/rtk-awareness.md b/hooks/claude/rtk-awareness.md index 0eaf3d52a..ec225a525 100644 --- a/hooks/claude/rtk-awareness.md +++ b/hooks/claude/rtk-awareness.md @@ -1,29 +1,8 @@ -# RTK - Rust Token Killer +# RTK quick awareness -**Usage**: Token-optimized CLI proxy (60-90% savings on dev operations) +Use RTK-prefixed commands for compact output. -## Meta Commands (always use rtk directly) - -```bash -rtk gain # Show token savings analytics -rtk gain --history # Show command usage history with savings -rtk discover # Analyze Claude Code history for missed opportunities -rtk proxy # Execute raw command without filtering (for debugging) -``` - -## Installation Verification - -```bash -rtk --version # Should show: rtk X.Y.Z -rtk gain # Should work (not "command not found") -which rtk # Verify correct binary -``` - -⚠️ **Name collision**: If `rtk gain` fails, you may have reachingforthejack/rtk (Rust Type Kit) installed instead. - -## Hook-Based Usage - -All other commands are automatically rewritten by the Claude Code hook. -Example: `git status` → `rtk git status` (transparent, 0 tokens overhead) - -Refer to CLAUDE.md for full command reference. +Examples: +- `rtk git status` +- `rtk cargo test` +- `rtk grep "pattern" .` diff --git a/hooks/cline/rules.md b/hooks/cline/rules.md index 7d6c8aff5..535f2988e 100644 --- a/hooks/cline/rules.md +++ b/hooks/cline/rules.md @@ -1,32 +1,7 @@ -# RTK - Rust Token Killer (Cline) +# RTK rules -**Usage**: Token-optimized CLI proxy for shell commands. +Prefer RTK wrappers for shell commands. -## Rule - -Always prefix shell commands with `rtk` to minimize token consumption. - -Examples: - -```bash -rtk git status -rtk cargo test -rtk ls src/ -rtk grep "pattern" src/ -rtk find "*.rs" . -rtk docker ps -rtk gh pr list -``` - -## Meta Commands - -```bash -rtk gain # Show token savings -rtk gain --history # Command history with savings -rtk discover # Find missed RTK opportunities -rtk proxy # Run raw (no filtering, for debugging) -``` - -## Why - -RTK filters and compresses command output before it reaches the LLM context, saving 60-90% tokens on common operations. Always use `rtk ` instead of raw commands. +- `rtk git ...` +- `rtk cargo ...` +- `rtk grep ...` diff --git a/hooks/codex/rtk-awareness.md b/hooks/codex/rtk-awareness.md index 7ae285e1a..ec225a525 100644 --- a/hooks/codex/rtk-awareness.md +++ b/hooks/codex/rtk-awareness.md @@ -1,32 +1,8 @@ -# RTK - Rust Token Killer (Codex CLI) +# RTK quick awareness -**Usage**: Token-optimized CLI proxy for shell commands. - -## Rule - -Always prefix shell commands with `rtk`. +Use RTK-prefixed commands for compact output. Examples: - -```bash -rtk git status -rtk cargo test -rtk npm run build -rtk pytest -q -``` - -## Meta Commands - -```bash -rtk gain # Token savings analytics -rtk gain --history # Recent command savings history -rtk proxy # Run raw command without filtering -``` - -## Verification - -```bash -rtk --version -rtk gain -which rtk -``` +- `rtk git status` +- `rtk cargo test` +- `rtk grep "pattern" .` diff --git a/hooks/copilot/rtk-awareness.md b/hooks/copilot/rtk-awareness.md index 185f460c5..ec225a525 100644 --- a/hooks/copilot/rtk-awareness.md +++ b/hooks/copilot/rtk-awareness.md @@ -1,60 +1,8 @@ -# RTK — Copilot Integration (VS Code Copilot Chat + Copilot CLI) +# RTK quick awareness -**Usage**: Token-optimized CLI proxy (60-90% savings on dev operations) +Use RTK-prefixed commands for compact output. -## What's automatic - -The `.github/copilot-instructions.md` file is loaded at session start by both Copilot CLI and VS Code Copilot Chat. -It instructs Copilot to prefix commands with `rtk` automatically. - -The `.github/hooks/rtk-rewrite.json` hook adds a `PreToolUse` safety net via `rtk hook` — -a cross-platform Rust binary that intercepts raw bash tool calls and rewrites them. -No shell scripts, no `jq` dependency, works on Windows natively. - -## Meta commands (always use directly) - -```bash -rtk gain # Token savings dashboard for this session -rtk gain --history # Per-command history with savings % -rtk discover # Scan session history for missed rtk opportunities -rtk proxy # Run raw (no filtering) but still track it -``` - -## Installation verification - -```bash -rtk --version # Should print: rtk X.Y.Z -rtk gain # Should show a dashboard (not "command not found") -which rtk # Verify correct binary path -``` - -> ⚠️ **Name collision**: If `rtk gain` fails, you may have `reachingforthejack/rtk` -> (Rust Type Kit) installed instead. Check `which rtk` and reinstall from rtk-ai/rtk. - -## How the hook works - -`rtk hook` reads `PreToolUse` JSON from stdin, detects the agent format, and responds appropriately: - -**VS Code Copilot Chat** (supports `updatedInput` — transparent rewrite, no denial): -1. Agent runs `git status` → `rtk hook` intercepts via `PreToolUse` -2. `rtk hook` detects VS Code format (`tool_name`/`tool_input` keys) -3. Returns `hookSpecificOutput.updatedInput.command = "rtk git status"` -4. Agent runs the rewritten command silently — no denial, no retry - -**GitHub Copilot CLI** (deny-with-suggestion — CLI ignores `updatedInput` today, see [issue #2013](https://github.com/github/copilot-cli/issues/2013)): -1. Agent runs `git status` → `rtk hook` intercepts via `PreToolUse` -2. `rtk hook` detects Copilot CLI format (`toolName`/`toolArgs` keys) -3. Returns `permissionDecision: deny` with reason: `"Token savings: use 'rtk git status' instead"` -4. Copilot reads the reason and re-runs `rtk git status` - -When Copilot CLI adds `updatedInput` support, only `rtk hook` needs updating — no config changes. - -## Integration comparison - -| Tool | Mechanism | Hook output | File | -|-----------------------|-----------------------------------------|--------------------------|------------------------------------| -| Claude Code | `PreToolUse` hook with `updatedInput` | Transparent rewrite | `hooks/rtk-rewrite.sh` | -| VS Code Copilot Chat | `PreToolUse` hook with `updatedInput` | Transparent rewrite | `.github/hooks/rtk-rewrite.json` | -| GitHub Copilot CLI | `PreToolUse` deny-with-suggestion | Denial + retry | `.github/hooks/rtk-rewrite.json` | -| OpenCode | Plugin `tool.execute.before` | Transparent rewrite | `hooks/opencode-rtk.ts` | -| (any) | Custom instructions | Prompt-level guidance | `.github/copilot-instructions.md` | +Examples: +- `rtk git status` +- `rtk cargo test` +- `rtk grep "pattern" .` diff --git a/hooks/kilocode/rules.md b/hooks/kilocode/rules.md index 53d764596..b24318875 100644 --- a/hooks/kilocode/rules.md +++ b/hooks/kilocode/rules.md @@ -21,9 +21,6 @@ rtk gh pr list ## Meta Commands ```bash -rtk gain # Show token savings -rtk gain --history # Command history with savings -rtk discover # Find missed RTK opportunities rtk proxy # Run raw (no filtering, for debugging) ``` diff --git a/hooks/windsurf/rules.md b/hooks/windsurf/rules.md index 8491f5a78..535f2988e 100644 --- a/hooks/windsurf/rules.md +++ b/hooks/windsurf/rules.md @@ -1,32 +1,7 @@ -# RTK - Rust Token Killer (Windsurf) +# RTK rules -**Usage**: Token-optimized CLI proxy for shell commands. +Prefer RTK wrappers for shell commands. -## Rule - -Always prefix shell commands with `rtk` to minimize token consumption. - -Examples: - -```bash -rtk git status -rtk cargo test -rtk ls src/ -rtk grep "pattern" src/ -rtk find "*.rs" . -rtk docker ps -rtk gh pr list -``` - -## Meta Commands - -```bash -rtk gain # Show token savings -rtk gain --history # Command history with savings -rtk discover # Find missed RTK opportunities -rtk proxy # Run raw (no filtering, for debugging) -``` - -## Why - -RTK filters and compresses command output before it reaches the LLM context, saving 60-90% tokens on common operations. Always use `rtk ` instead of raw commands. +- `rtk git ...` +- `rtk cargo ...` +- `rtk grep ...` diff --git a/openclaw/README.md b/openclaw/README.md deleted file mode 100644 index 301d7c0fa..000000000 --- a/openclaw/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# RTK Plugin for OpenClaw - -Transparently rewrites shell commands executed via OpenClaw's `exec` tool to their RTK equivalents, achieving 60-90% LLM token savings. - -This is the OpenClaw equivalent of the Claude Code hooks in `hooks/rtk-rewrite.sh`. - -## How it works - -The plugin registers a `before_tool_call` hook that intercepts `exec` tool calls. When the agent runs a command like `git status`, the plugin delegates to `rtk rewrite` which returns the optimized command (e.g. `rtk git status`). The compressed output enters the agent's context window, saving tokens. - -All rewrite logic lives in RTK itself (`rtk rewrite`). This plugin is a thin delegate -- when new filters are added to RTK, the plugin picks them up automatically with zero changes. - -## Installation - -### Prerequisites - -RTK must be installed and available in `$PATH`: - -```bash -brew install rtk -# or -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh -``` - -### Install the plugin - -```bash -# Copy the plugin to OpenClaw's extensions directory -mkdir -p ~/.openclaw/extensions/rtk-rewrite -cp openclaw/index.ts openclaw/openclaw.plugin.json ~/.openclaw/extensions/rtk-rewrite/ - -# Restart the gateway -openclaw gateway restart -``` - -### Or install via OpenClaw CLI - -```bash -openclaw plugins install ./openclaw -``` - -## Configuration - -In `openclaw.json`: - -```json5 -{ - plugins: { - entries: { - "rtk-rewrite": { - enabled: true, - config: { - enabled: true, // Toggle rewriting on/off - verbose: false // Log rewrites to console - } - } - } - } -} -``` - -## What gets rewritten - -Everything that `rtk rewrite` supports (30+ commands). See the [full command list](https://github.com/rtk-ai/rtk#commands). - -## What's NOT rewritten - -Handled by `rtk rewrite` guards: -- Commands already using `rtk` -- Piped commands (`|`, `&&`, `;`) -- Heredocs (`<<`) -- Commands without an RTK filter - -## Measured savings - -| Command | Token savings | -|---------|--------------| -| `git log --stat` | 87% | -| `ls -la` | 78% | -| `git status` | 66% | -| `grep` (single file) | 52% | -| `find -name` | 48% | - -## License - -MIT -- same as RTK. diff --git a/openclaw/index.ts b/openclaw/index.ts deleted file mode 100644 index 17ea4ec93..000000000 --- a/openclaw/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * RTK Rewrite Plugin for OpenClaw - * - * Transparently rewrites exec tool commands to RTK equivalents - * before execution, achieving 60-90% LLM token savings. - * - * All rewrite logic lives in `rtk rewrite` (src/discover/registry.rs). - * This plugin is a thin delegate — to add or change rules, edit the - * Rust registry, not this file. - */ - -import { execSync } from "node:child_process"; - -let rtkAvailable: boolean | null = null; - -function checkRtk(): boolean { - if (rtkAvailable !== null) return rtkAvailable; - try { - execSync("which rtk", { stdio: "ignore" }); - rtkAvailable = true; - } catch { - rtkAvailable = false; - } - return rtkAvailable; -} - -function tryRewrite(command: string): string | null { - try { - const result = execSync(`rtk rewrite ${JSON.stringify(command)}`, { - encoding: "utf-8", - timeout: 2000, - }).trim(); - return result && result !== command ? result : null; - } catch { - return null; - } -} - -export default function register(api: any) { - const pluginConfig = api.config ?? {}; - const enabled = pluginConfig.enabled !== false; - const verbose = pluginConfig.verbose === true; - - if (!enabled) return; - - if (!checkRtk()) { - console.warn("[rtk] rtk binary not found in PATH — plugin disabled"); - return; - } - - api.on( - "before_tool_call", - (event: { toolName: string; params: Record }) => { - if (event.toolName !== "exec") return; - - const command = event.params?.command; - if (typeof command !== "string") return; - - const rewritten = tryRewrite(command); - if (!rewritten) return; - - if (verbose) { - console.log(`[rtk] ${command} -> ${rewritten}`); - } - - return { params: { ...event.params, command: rewritten } }; - }, - { priority: 10 } - ); - - if (verbose) { - console.log("[rtk] OpenClaw plugin registered"); - } -} diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json deleted file mode 100644 index 3fce418d7..000000000 --- a/openclaw/openclaw.plugin.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "rtk-rewrite", - "name": "RTK Token Optimizer", - "version": "1.0.0", - "description": "Transparently rewrites shell commands to their RTK equivalents for 60-90% LLM token savings", - "homepage": "https://github.com/rtk-ai/rtk", - "license": "MIT", - "configSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable automatic command rewriting to RTK equivalents" - }, - "verbose": { - "type": "boolean", - "default": false, - "description": "Log rewrite decisions to console for debugging" - } - } - }, - "uiHints": { - "enabled": { "label": "Enable RTK rewriting" }, - "verbose": { "label": "Verbose logging" } - } -} diff --git a/openclaw/package.json b/openclaw/package.json deleted file mode 100644 index 18d359ff4..000000000 --- a/openclaw/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@rtk-ai/rtk-rewrite", - "version": "1.0.0", - "description": "RTK plugin for OpenClaw — rewrites shell commands for 60-90% LLM token savings", - "main": "index.ts", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/rtk-ai/rtk", - "directory": "openclaw" - }, - "homepage": "https://github.com/rtk-ai/rtk", - "keywords": [ - "rtk", - "openclaw", - "openclaw-plugin", - "token-savings", - "llm", - "cli-proxy" - ], - "files": [ - "index.ts", - "openclaw.plugin.json", - "README.md" - ], - "peerDependencies": { - "rtk": ">=0.28.0" - } -} diff --git a/scripts/check-installation.sh b/scripts/check-installation.sh index e7a56fb7a..bee964664 100755 --- a/scripts/check-installation.sh +++ b/scripts/check-installation.sh @@ -37,7 +37,7 @@ echo "" # Check 3: Is it Token Killer or Type Kit? echo "3. Verifying this is Token Killer (not Type Kit)..." -if rtk gain &>/dev/null || rtk gain --help &>/dev/null; then +if rtk --help &>/dev/null; then echo -e " ${GREEN}✅ CORRECT - You have Rust Token Killer${NC}" CORRECT_RTK=true else @@ -74,7 +74,6 @@ check_command() { fi } -check_command "gain" "Token savings analytics" check_command "git" "Git operations" check_command "gh" "GitHub CLI" check_command "pnpm" "pnpm support" diff --git a/scripts/prek/check-architecture-size.sh b/scripts/prek/check-architecture-size.sh new file mode 100755 index 000000000..6f9e93696 --- /dev/null +++ b/scripts/prek/check-architecture-size.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Soft caps to prevent further growth of known hotspots. +# Tight enough to force extraction/refactor on growth spikes. + +# Use parallel arrays for bash 3.2 compatibility (no associative arrays) +FILES=("src/main.rs" "src/hooks/init.rs" "src/discover/registry.rs" "src/cmds/git/git.rs" "src/cmds/cloud/aws_cmd.rs") +CAPES=("2950" "4050" "3600" "2900" "2850") + +fail=0 +for i in $(seq 0 $((${#FILES[@]} - 1))); do + file="${FILES[$i]}" + limit="${CAPES[$i]}" + [ -f "$file" ] || continue + lines=$(wc -l < "$file" | tr -d ' ') + if [ "$lines" -gt "$limit" ]; then + echo "FAIL [$file] line count $lines exceeds cap $limit" + fail=1 + fi +done + +if [ "$fail" -ne 0 ]; then + echo "architecture-size-guard: FAILED" + exit 1 +fi + +echo "architecture-size-guard: OK" diff --git a/scripts/prek/check-cmd-tests.sh b/scripts/prek/check-cmd-tests.sh new file mode 100755 index 000000000..05eb50e9c --- /dev/null +++ b/scripts/prek/check-cmd-tests.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail=0 + +for file in "$@"; do + [[ "$file" =~ ^src/cmds/.+_cmd\.rs$ ]] || continue + [ -f "$file" ] || continue + + if ! rg -n '^\s*#\[cfg\(test\)\]' "$file" >/dev/null; then + echo "FAIL [$file] missing #[cfg(test)] module" + fail=1 + fi +done + +if [ "$fail" -ne 0 ]; then + echo "cmd-module-has-tests: FAILED" + exit 1 +fi + +echo "cmd-module-has-tests: OK" diff --git a/scripts/prek/check-dangerous-patterns.sh b/scripts/prek/check-dangerous-patterns.sh new file mode 100755 index 000000000..0aa6cdb88 --- /dev/null +++ b/scripts/prek/check-dangerous-patterns.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail=0 + +check_added_lines() { + local file="$1" + local added + added=$(jj diff --color never --git -- "$file" | sed -n 's/^+//p' | grep -v '^+++' || true) + + [ -z "$added" ] && return 0 + + # Security-sensitive shell spawning patterns. + if echo "$added" | rg -n 'Command::new\("(sh|bash)"\)' >/dev/null; then + echo "FAIL [$file] direct shell spawn detected (Command::new(\"sh\"|\"bash\"))." + fail=1 + fi + + # Panic/debug markers in production code. + if echo "$added" | rg -n '(^|[^[:alnum:]_])(todo!|unimplemented!|dbg!)\s*\(' >/dev/null; then + echo "FAIL [$file] todo!/unimplemented!/dbg! added." + fail=1 + fi + + # Suspicious env mutation patterns. + if echo "$added" | rg -n '\.env\("(LD_PRELOAD|PATH)"' >/dev/null; then + echo "FAIL [$file] .env(\"LD_PRELOAD\"|\"PATH\") added." + fail=1 + fi +} + +for f in "$@"; do + [[ "$f" == *.rs ]] || continue + [ -f "$f" ] || continue + check_added_lines "$f" +done + +if [ "$fail" -ne 0 ]; then + echo "dangerous-patterns-in-added-rust-lines: FAILED" + exit 1 +fi + +echo "dangerous-patterns-in-added-rust-lines: OK" diff --git a/scripts/prek/check-new-command-wiring.sh b/scripts/prek/check-new-command-wiring.sh new file mode 100755 index 000000000..283ac9e96 --- /dev/null +++ b/scripts/prek/check-new-command-wiring.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# If a new src/cmds/*_cmd.rs is added, require routing/wiring updates. +# Enforces command architecture coupling: +# - src/main.rs (CLI wiring) +# - src/discover/rules.rs (rewrite routing) + +status_lines=$(jj status --color never | sed -n 's/^\([AMD]\) \(.*\)$/\1 \2/p') + +added_cmds=$(echo "$status_lines" | awk '$1=="A" && $2 ~ /^src\/cmds\/.+_cmd\.rs$/ {print $2}') + +if [ -z "$added_cmds" ]; then + echo "new-command-wiring-guard: OK (no new *_cmd.rs files)" + exit 0 +fi + +changed_files=$(echo "$status_lines" | awk '{print $2}') + +missing=0 +if ! echo "$changed_files" | rg -n '^src/main\.rs$' >/dev/null; then + echo "FAIL new command module added, but src/main.rs was not updated" + missing=1 +fi +if ! echo "$changed_files" | rg -n '^src/discover/rules\.rs$' >/dev/null; then + echo "FAIL new command module added, but src/discover/rules.rs was not updated" + missing=1 +fi + +if [ "$missing" -ne 0 ]; then + echo "new-command-wiring-guard: FAILED" + echo "Added command modules:" + echo "$added_cmds" | sed 's/^/ - /' + exit 1 +fi + +echo "new-command-wiring-guard: OK" diff --git a/scripts/prek/run-cargo-audit.sh b/scripts/prek/run-cargo-audit.sh new file mode 100755 index 000000000..5174191d0 --- /dev/null +++ b/scripts/prek/run-cargo-audit.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v cargo-audit >/dev/null 2>&1; then + echo "FAIL cargo-audit not installed." + echo "Install with: cargo install cargo-audit" + exit 1 +fi + +cargo audit diff --git a/scripts/rtk-economics.sh b/scripts/rtk-economics.sh deleted file mode 100755 index 01aa3b1a5..000000000 --- a/scripts/rtk-economics.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash -# rtk-economics.sh -# Combine ccusage (tokens spent) with rtk (tokens saved) for economic analysis - -set -euo pipefail - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Get current month -CURRENT_MONTH=$(date +%Y-%m) - -echo -e "${BLUE}📊 RTK Economic Impact Analysis${NC}" -echo "════════════════════════════════════════════════════════════════" -echo - -# Check if ccusage is available -if ! command -v ccusage &> /dev/null; then - echo -e "${RED}Error: ccusage not found${NC}" - echo "Install: npm install -g @anthropics/claude-code-usage" - exit 1 -fi - -# Check if rtk is available -if ! command -v rtk &> /dev/null; then - echo -e "${RED}Error: rtk not found${NC}" - echo "Install: cargo install --path ." - exit 1 -fi - -# Fetch ccusage data -echo -e "${YELLOW}Fetching token usage data from ccusage...${NC}" -if ! ccusage_json=$(ccusage monthly --json 2>/dev/null); then - echo -e "${RED}Failed to fetch ccusage data${NC}" - exit 1 -fi - -# Fetch rtk data -echo -e "${YELLOW}Fetching token savings data from rtk...${NC}" -if ! rtk_json=$(rtk gain --monthly --format json 2>/dev/null); then - echo -e "${RED}Failed to fetch rtk data${NC}" - exit 1 -fi - -echo - -# Parse ccusage data for current month -ccusage_cost=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .totalCost // 0") -ccusage_input=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .inputTokens // 0") -ccusage_output=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .outputTokens // 0") -ccusage_total=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .totalTokens // 0") - -# Parse rtk data for current month -rtk_saved=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .saved_tokens // 0") -rtk_commands=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .commands // 0") -rtk_input=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .input_tokens // 0") -rtk_output=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .output_tokens // 0") -rtk_pct=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .savings_pct // 0") - -# Estimate cost avoided (rough: $0.0001/token for mixed usage) -# More accurate would be to use ccusage's model-specific pricing -saved_cost=$(echo "scale=2; $rtk_saved * 0.0001" | bc 2>/dev/null || echo "0") - -# Calculate total without rtk -total_without_rtk=$(echo "scale=2; $ccusage_cost + $saved_cost" | bc 2>/dev/null || echo "$ccusage_cost") - -# Calculate savings percentage -if (( $(echo "$total_without_rtk > 0" | bc -l) )); then - savings_pct=$(echo "scale=1; ($saved_cost / $total_without_rtk) * 100" | bc 2>/dev/null || echo "0") -else - savings_pct="0" -fi - -# Calculate cost per command -if [ "$rtk_commands" -gt 0 ]; then - cost_per_cmd_with=$(echo "scale=2; $ccusage_cost / $rtk_commands" | bc 2>/dev/null || echo "0") - cost_per_cmd_without=$(echo "scale=2; $total_without_rtk / $rtk_commands" | bc 2>/dev/null || echo "0") -else - cost_per_cmd_with="N/A" - cost_per_cmd_without="N/A" -fi - -# Format numbers -format_number() { - local num=$1 - if [ "$num" = "0" ] || [ "$num" = "N/A" ]; then - echo "$num" - else - echo "$num" | numfmt --to=si 2>/dev/null || echo "$num" - fi -} - -# Display report -cat << EOF -${GREEN}💰 Economic Impact Report - $CURRENT_MONTH${NC} -════════════════════════════════════════════════════════════════ - -${BLUE}Tokens Consumed (via Claude API):${NC} - Input tokens: $(format_number $ccusage_input) - Output tokens: $(format_number $ccusage_output) - Total tokens: $(format_number $ccusage_total) - ${RED}Actual cost: \$$ccusage_cost${NC} - -${BLUE}Tokens Saved by rtk:${NC} - Commands executed: $rtk_commands - Input avoided: $(format_number $rtk_input) tokens - Output generated: $(format_number $rtk_output) tokens - Total saved: $(format_number $rtk_saved) tokens (${rtk_pct}% reduction) - ${GREEN}Cost avoided: ~\$$saved_cost${NC} - -${BLUE}Economic Analysis:${NC} - Cost without rtk: \$$total_without_rtk (estimated) - Cost with rtk: \$$ccusage_cost (actual) - ${GREEN}Net savings: \$$saved_cost ($savings_pct%)${NC} - ROI: ${GREEN}Infinite${NC} (rtk is free) - -${BLUE}Efficiency Metrics:${NC} - Cost per command: \$$cost_per_cmd_without → \$$cost_per_cmd_with - Tokens per command: $(echo "scale=0; $rtk_input / $rtk_commands" | bc 2>/dev/null || echo "N/A") → $(echo "scale=0; $rtk_output / $rtk_commands" | bc 2>/dev/null || echo "N/A") - -${BLUE}12-Month Projection:${NC} - Annual savings: ~\$$(echo "scale=2; $saved_cost * 12" | bc 2>/dev/null || echo "0") - Commands needed: $(echo "$rtk_commands * 12" | bc 2>/dev/null || echo "0") (at current rate) - -════════════════════════════════════════════════════════════════ - -${YELLOW}Note:${NC} Cost estimates use \$0.0001/token average. Actual pricing varies by model. -See ccusage for precise model-specific costs. - -${GREEN}Recommendation:${NC} Focus rtk usage on high-frequency commands (git, grep, ls) -for maximum cost reduction. - -EOF diff --git a/scripts/test-all.sh b/scripts/test-all.sh index f0e2c06b1..8a57ab93d 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -336,8 +336,6 @@ assert_ok "rtk test echo ok" rtk test echo ok section "Gain" -assert_ok "rtk gain" rtk gain -assert_ok "rtk gain --history" rtk gain --history # ── 21. Config & Init ──────────────────────────────── @@ -470,14 +468,11 @@ assert_ok "rtk --skip-env npm --help" rtk --skip-env npm --help section "CcEconomics" -assert_ok "rtk cc-economics" rtk cc-economics # ── 33. Learn ─────────────────────────────────────── section "Learn" -assert_ok "rtk learn --help" rtk learn --help -assert_ok "rtk learn (no sessions)" rtk learn --since 0 2>&1 || true # ── 32. Rewrite ─────────────────────────────────────── @@ -521,7 +516,6 @@ assert_contains "rtk proxy passthrough" "hello" rtk proxy echo hello section "Discover" -assert_ok "rtk discover" rtk discover # ── 36. Diff ────────────────────────────────────────── diff --git a/scripts/test-aristote.sh b/scripts/test-aristote.sh index 371ce9033..ad25a9185 100755 --- a/scripts/test-aristote.sh +++ b/scripts/test-aristote.sh @@ -205,8 +205,6 @@ assert_ok "rtk err ls" rtk err ls "$ARISTOTE/src" section "Gain (after above commands)" -assert_ok "rtk gain" rtk gain -assert_ok "rtk gain --history" rtk gain --history # ══════════════════════════════════════════════════════ # Report diff --git a/scripts/test-tracking.sh b/scripts/test-tracking.sh deleted file mode 100755 index 5faaf89a7..000000000 --- a/scripts/test-tracking.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -# Test tracking end-to-end: run commands, verify they appear in rtk gain --history -set -euo pipefail - -# Workaround for macOS bash pipe handling in strict mode -set +e # Allow errors in pipe chains to continue - -PASS=0; FAIL=0; FAILURES=() -RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m' - -check() { - local name="$1" needle="$2" - shift 2 - local output - if output=$("$@" 2>&1) && echo "$output" | grep -q "$needle"; then - PASS=$((PASS+1)); printf " ${GREEN}PASS${NC} %s\n" "$name" - else - FAIL=$((FAIL+1)); FAILURES+=("$name") - printf " ${RED}FAIL${NC} %s\n" "$name" - printf " expected: '%s'\n" "$needle" - printf " got: %s\n" "$(echo "$output" | head -3)" - fi -} - -echo "═══ RTK Tracking Validation ═══" -echo "" - -# 1. Commandes avec filtrage réel — doivent apparaitre dans history -echo "── Optimized commands (token savings) ──" -rtk ls . >/dev/null 2>&1 -check "rtk ls tracked" "rtk ls" rtk gain --history - -rtk git status >/dev/null 2>&1 -check "rtk git status tracked" "rtk git status" rtk gain --history - -rtk git log -5 >/dev/null 2>&1 -check "rtk git log tracked" "rtk git log" rtk gain --history - -# Git passthrough (timing-only) -echo "" -echo "── Passthrough commands (timing-only) ──" -rtk git tag --list >/dev/null 2>&1 -check "git passthrough tracked" "git tag --list" rtk gain --history - -# gh commands (if authenticated) -echo "" -echo "── GitHub CLI tracking ──" -if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then - rtk gh pr list >/dev/null 2>&1 || true - check "rtk gh pr list tracked" "rtk gh pr" rtk gain --history - - rtk gh run list >/dev/null 2>&1 || true - check "rtk gh run list tracked" "rtk gh run" rtk gain --history -else - echo " SKIP gh (not authenticated)" -fi - -# Stdin commands -echo "" -echo "── Stdin commands ──" -echo -e "line1\nline2\nline1\nERROR: bad\nline1" | rtk log >/dev/null 2>&1 -check "rtk log stdin tracked" "rtk log" rtk gain --history - -# Summary — verify passthrough doesn't dilute -echo "" -echo "── Summary integrity ──" -output=$(rtk gain 2>&1) -if echo "$output" | grep -q "Tokens saved"; then - PASS=$((PASS+1)); printf " ${GREEN}PASS${NC} rtk gain summary works\n" -else - FAIL=$((FAIL+1)); printf " ${RED}FAIL${NC} rtk gain summary\n" -fi - -echo "" -echo "═══ Results: ${PASS} passed, ${FAIL} failed ═══" -if [ ${#FAILURES[@]} -gt 0 ]; then - echo "Failures: ${FAILURES[*]}" -fi -exit $FAIL diff --git a/src/analytics/README.md b/src/analytics/README.md deleted file mode 100644 index 5cea9ce77..000000000 --- a/src/analytics/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Analytics - -> See also [docs/contributing/TECHNICAL.md](../../docs/contributing/TECHNICAL.md) for the full architecture overview - -## Scope - -**Read-only dashboards** over the tracking database. Queries token savings, correlates with external spending data, and surfaces adoption metrics. Never modifies the tracking DB. - -Owns: `rtk gain` (savings dashboard), `rtk cc-economics` (cost reduction), `rtk session` (adoption analysis), and Claude Code usage data parsing. - -Does **not** own: recording token savings (that's `core/tracking` called by `cmds/`), or command filtering itself (that's `cmds/`). - -Boundary rule: if a new module writes to the DB, it belongs in `core/` or `cmds/`, not here. Tool-specific analytics (like `cc_economics` reading Claude Code data) are fine — the boundary is "read-only presentation", not "tool-agnostic". - -## Purpose -Token savings analytics, economic modeling, and adoption metrics. - -These modules read from the SQLite tracking database to produce dashboards, spending estimates, and session-level adoption reports. - -## Adding New Functionality -To add a new analytics view: (1) create a new `*_cmd.rs` file in this directory, (2) query `core/tracking` for the metrics you need using the existing `TrackingDb` API, (3) register the command in `main.rs` under the `Commands` enum, and (4) add `#[cfg(test)]` unit tests with sample tracking data. Analytics modules should be read-only against the tracking database and never modify it. diff --git a/src/analytics/cc_economics.rs b/src/analytics/cc_economics.rs deleted file mode 100644 index 037593102..000000000 --- a/src/analytics/cc_economics.rs +++ /dev/null @@ -1,1155 +0,0 @@ -//! Claude Code Economics: Spending vs Savings Analysis -//! -//! Combines ccusage (tokens spent) with rtk tracking (tokens saved) to provide -//! dual-metric economic impact reporting with blended and active cost-per-token. - -use anyhow::{Context, Result}; -use chrono::NaiveDate; -use serde::Serialize; -use std::collections::HashMap; - -use super::ccusage::{self, CcusagePeriod, Granularity}; -use crate::core::tracking::{DayStats, MonthStats, Tracker, WeekStats}; -use crate::core::utils::{format_cpt, format_tokens, format_usd}; - -// ── Constants ── - -// API pricing ratios (verified Feb 2026, consistent across Claude models <=200K context) -// Source: https://docs.anthropic.com/en/docs/about-claude/models -const WEIGHT_OUTPUT: f64 = 5.0; // Output = 5x input -const WEIGHT_CACHE_CREATE: f64 = 1.25; // Cache write = 1.25x input -const WEIGHT_CACHE_READ: f64 = 0.1; // Cache read = 0.1x input - -// ── Types ── - -#[derive(Debug, Serialize)] -pub struct PeriodEconomics { - pub label: String, - // ccusage metrics (Option for graceful degradation) - pub cc_cost: Option, - pub cc_total_tokens: Option, - pub cc_active_tokens: Option, // input + output only (excluding cache) - // Per-type token breakdown - pub cc_input_tokens: Option, - pub cc_output_tokens: Option, - pub cc_cache_create_tokens: Option, - pub cc_cache_read_tokens: Option, - // rtk metrics - pub rtk_commands: Option, - pub rtk_saved_tokens: Option, - pub rtk_savings_pct: Option, - // Primary metric (weighted input CPT) - pub weighted_input_cpt: Option, // Derived input CPT using API ratios - pub savings_weighted: Option, // saved * weighted_input_cpt (PRIMARY) - // Legacy metrics (verbose mode only) - pub blended_cpt: Option, // cost / total_tokens (diluted by cache) - pub active_cpt: Option, // cost / active_tokens (OVERESTIMATES) - pub savings_blended: Option, // saved * blended_cpt (UNDERESTIMATES) - pub savings_active: Option, // saved * active_cpt (OVERESTIMATES) -} - -impl PeriodEconomics { - fn new(label: &str) -> Self { - Self { - label: label.to_string(), - cc_cost: None, - cc_total_tokens: None, - cc_active_tokens: None, - cc_input_tokens: None, - cc_output_tokens: None, - cc_cache_create_tokens: None, - cc_cache_read_tokens: None, - rtk_commands: None, - rtk_saved_tokens: None, - rtk_savings_pct: None, - weighted_input_cpt: None, - savings_weighted: None, - blended_cpt: None, - active_cpt: None, - savings_blended: None, - savings_active: None, - } - } - - fn set_ccusage(&mut self, metrics: &ccusage::CcusageMetrics) { - self.cc_cost = Some(metrics.total_cost); - self.cc_total_tokens = Some(metrics.total_tokens); - - // Store per-type tokens - self.cc_input_tokens = Some(metrics.input_tokens); - self.cc_output_tokens = Some(metrics.output_tokens); - self.cc_cache_create_tokens = Some(metrics.cache_creation_tokens); - self.cc_cache_read_tokens = Some(metrics.cache_read_tokens); - - // Active tokens (legacy) - let active = metrics.input_tokens + metrics.output_tokens; - self.cc_active_tokens = Some(active); - } - - fn set_rtk_from_day(&mut self, stats: &DayStats) { - self.rtk_commands = Some(stats.commands); - self.rtk_saved_tokens = Some(stats.saved_tokens); - self.rtk_savings_pct = Some(stats.savings_pct); - } - - fn set_rtk_from_week(&mut self, stats: &WeekStats) { - self.rtk_commands = Some(stats.commands); - self.rtk_saved_tokens = Some(stats.saved_tokens); - self.rtk_savings_pct = Some(stats.savings_pct); - } - - fn set_rtk_from_month(&mut self, stats: &MonthStats) { - self.rtk_commands = Some(stats.commands); - self.rtk_saved_tokens = Some(stats.saved_tokens); - self.rtk_savings_pct = Some(if stats.input_tokens + stats.output_tokens > 0 { - stats.saved_tokens as f64 - / (stats.saved_tokens + stats.input_tokens + stats.output_tokens) as f64 - * 100.0 - } else { - 0.0 - }); - } - - fn compute_weighted_metrics(&mut self) { - // Weighted input CPT derivation using API price ratios - if let (Some(cost), Some(saved)) = (self.cc_cost, self.rtk_saved_tokens) { - if let (Some(input), Some(output), Some(cache_create), Some(cache_read)) = ( - self.cc_input_tokens, - self.cc_output_tokens, - self.cc_cache_create_tokens, - self.cc_cache_read_tokens, - ) { - // Weighted units = input + 5*output + 1.25*cache_create + 0.1*cache_read - let weighted_units = input as f64 - + WEIGHT_OUTPUT * output as f64 - + WEIGHT_CACHE_CREATE * cache_create as f64 - + WEIGHT_CACHE_READ * cache_read as f64; - - if weighted_units > 0.0 { - let input_cpt = cost / weighted_units; - let savings = saved as f64 * input_cpt; - - self.weighted_input_cpt = Some(input_cpt); - self.savings_weighted = Some(savings); - } - } - } - } - - fn compute_dual_metrics(&mut self) { - if let (Some(cost), Some(saved)) = (self.cc_cost, self.rtk_saved_tokens) { - // Blended CPT (cost / total_tokens including cache) - if let Some(total) = self.cc_total_tokens { - if total > 0 { - self.blended_cpt = Some(cost / total as f64); - self.savings_blended = Some(saved as f64 * (cost / total as f64)); - } - } - - // Active CPT (cost / active_tokens = input+output only) - if let Some(active) = self.cc_active_tokens { - if active > 0 { - self.active_cpt = Some(cost / active as f64); - self.savings_active = Some(saved as f64 * (cost / active as f64)); - } - } - } - } -} - -#[derive(Debug, Serialize)] -struct Totals { - cc_cost: f64, - cc_total_tokens: u64, - cc_active_tokens: u64, - cc_input_tokens: u64, - cc_output_tokens: u64, - cc_cache_create_tokens: u64, - cc_cache_read_tokens: u64, - rtk_commands: usize, - rtk_saved_tokens: usize, - rtk_avg_savings_pct: f64, - weighted_input_cpt: Option, - savings_weighted: Option, - blended_cpt: Option, - active_cpt: Option, - savings_blended: Option, - savings_active: Option, -} - -// ── Public API ── - -pub fn run( - daily: bool, - weekly: bool, - monthly: bool, - all: bool, - format: &str, - verbose: u8, -) -> Result<()> { - let tracker = Tracker::new().context("Failed to initialize tracking database")?; - - match format { - "json" => export_json(&tracker, daily, weekly, monthly, all), - "csv" => export_csv(&tracker, daily, weekly, monthly, all), - _ => display_text(&tracker, daily, weekly, monthly, all, verbose), - } -} - -// ── Merge Logic ── - -fn merge_daily(cc: Option>, rtk: Vec) -> Vec { - let mut map: HashMap = HashMap::new(); - - // Insert ccusage data - if let Some(cc_data) = cc { - for entry in cc_data { - let super::ccusage::CcusagePeriod { key, metrics } = entry; - map.entry(key) - .or_insert_with_key(|k| PeriodEconomics::new(k)) - .set_ccusage(&metrics); - } - } - - // Merge rtk data - for entry in rtk { - map.entry(entry.date.clone()) - .or_insert_with_key(|k| PeriodEconomics::new(k)) - .set_rtk_from_day(&entry); - } - - // Compute dual metrics and sort - let mut result: Vec<_> = map.into_values().collect(); - for period in &mut result { - period.compute_weighted_metrics(); - period.compute_dual_metrics(); - } - result.sort_by(|a, b| a.label.cmp(&b.label)); - result -} - -fn merge_weekly(cc: Option>, rtk: Vec) -> Vec { - let mut map: HashMap = HashMap::new(); - - // Insert ccusage data (key = ISO Monday "2026-01-20") - if let Some(cc_data) = cc { - for entry in cc_data { - let super::ccusage::CcusagePeriod { key, metrics } = entry; - map.entry(key) - .or_insert_with_key(|k| PeriodEconomics::new(k)) - .set_ccusage(&metrics); - } - } - - // Merge rtk data (week_start = legacy Saturday "2026-01-18") - // Convert Saturday to Monday for alignment - for entry in rtk { - let monday_key = match convert_saturday_to_monday(&entry.week_start) { - Some(m) => m, - None => { - eprintln!("[warn] Invalid week_start format: {}", entry.week_start); - continue; - } - }; - - map.entry(monday_key) - .or_insert_with_key(|key| PeriodEconomics::new(key)) - .set_rtk_from_week(&entry); - } - - let mut result: Vec<_> = map.into_values().collect(); - for period in &mut result { - period.compute_weighted_metrics(); - period.compute_dual_metrics(); - } - result.sort_by(|a, b| a.label.cmp(&b.label)); - result -} - -fn merge_monthly(cc: Option>, rtk: Vec) -> Vec { - let mut map: HashMap = HashMap::new(); - - // Insert ccusage data - if let Some(cc_data) = cc { - for entry in cc_data { - let super::ccusage::CcusagePeriod { key, metrics } = entry; - map.entry(key) - .or_insert_with_key(|k| PeriodEconomics::new(k)) - .set_ccusage(&metrics); - } - } - - // Merge rtk data - for entry in rtk { - map.entry(entry.month.clone()) - .or_insert_with_key(|k| PeriodEconomics::new(k)) - .set_rtk_from_month(&entry); - } - - let mut result: Vec<_> = map.into_values().collect(); - for period in &mut result { - period.compute_weighted_metrics(); - period.compute_dual_metrics(); - } - result.sort_by(|a, b| a.label.cmp(&b.label)); - result -} - -// ── Helpers ── - -/// Convert Saturday week_start (legacy rtk) to ISO Monday -/// Example: "2026-01-18" (Sat) -> "2026-01-20" (Mon) -fn convert_saturday_to_monday(saturday: &str) -> Option { - let sat_date = NaiveDate::parse_from_str(saturday, "%Y-%m-%d").ok()?; - - // rtk uses Saturday as week start, ISO uses Monday - // Saturday + 2 days = Monday - let monday = sat_date + chrono::TimeDelta::try_days(2)?; - - Some(monday.format("%Y-%m-%d").to_string()) -} - -fn compute_totals(periods: &[PeriodEconomics]) -> Totals { - let mut totals = Totals { - cc_cost: 0.0, - cc_total_tokens: 0, - cc_active_tokens: 0, - cc_input_tokens: 0, - cc_output_tokens: 0, - cc_cache_create_tokens: 0, - cc_cache_read_tokens: 0, - rtk_commands: 0, - rtk_saved_tokens: 0, - rtk_avg_savings_pct: 0.0, - weighted_input_cpt: None, - savings_weighted: None, - blended_cpt: None, - active_cpt: None, - savings_blended: None, - savings_active: None, - }; - - let mut pct_sum = 0.0; - let mut pct_count = 0; - - for p in periods { - if let Some(cost) = p.cc_cost { - totals.cc_cost += cost; - } - if let Some(total) = p.cc_total_tokens { - totals.cc_total_tokens += total; - } - if let Some(active) = p.cc_active_tokens { - totals.cc_active_tokens += active; - } - if let Some(input) = p.cc_input_tokens { - totals.cc_input_tokens += input; - } - if let Some(output) = p.cc_output_tokens { - totals.cc_output_tokens += output; - } - if let Some(cache_create) = p.cc_cache_create_tokens { - totals.cc_cache_create_tokens += cache_create; - } - if let Some(cache_read) = p.cc_cache_read_tokens { - totals.cc_cache_read_tokens += cache_read; - } - if let Some(cmds) = p.rtk_commands { - totals.rtk_commands += cmds; - } - if let Some(saved) = p.rtk_saved_tokens { - totals.rtk_saved_tokens += saved; - } - if let Some(pct) = p.rtk_savings_pct { - pct_sum += pct; - pct_count += 1; - } - } - - if pct_count > 0 { - totals.rtk_avg_savings_pct = pct_sum / pct_count as f64; - } - - // Compute global weighted metrics - let weighted_units = totals.cc_input_tokens as f64 - + WEIGHT_OUTPUT * totals.cc_output_tokens as f64 - + WEIGHT_CACHE_CREATE * totals.cc_cache_create_tokens as f64 - + WEIGHT_CACHE_READ * totals.cc_cache_read_tokens as f64; - - if weighted_units > 0.0 { - let input_cpt = totals.cc_cost / weighted_units; - totals.weighted_input_cpt = Some(input_cpt); - totals.savings_weighted = Some(totals.rtk_saved_tokens as f64 * input_cpt); - } - - // Compute global dual metrics (legacy) - if totals.cc_total_tokens > 0 { - totals.blended_cpt = Some(totals.cc_cost / totals.cc_total_tokens as f64); - totals.savings_blended = Some(totals.rtk_saved_tokens as f64 * totals.blended_cpt.unwrap()); - } - if totals.cc_active_tokens > 0 { - totals.active_cpt = Some(totals.cc_cost / totals.cc_active_tokens as f64); - totals.savings_active = Some(totals.rtk_saved_tokens as f64 * totals.active_cpt.unwrap()); - } - - totals -} - -// ── Display ── - -fn display_text( - tracker: &Tracker, - daily: bool, - weekly: bool, - monthly: bool, - all: bool, - verbose: u8, -) -> Result<()> { - // Default: summary view - if !daily && !weekly && !monthly && !all { - display_summary(tracker, verbose)?; - return Ok(()); - } - - if all || daily { - display_daily(tracker, verbose)?; - } - if all || weekly { - display_weekly(tracker, verbose)?; - } - if all || monthly { - display_monthly(tracker, verbose)?; - } - - Ok(()) -} - -fn display_summary(tracker: &Tracker, verbose: u8) -> Result<()> { - let cc_monthly = - ccusage::fetch(Granularity::Monthly).context("Failed to fetch ccusage monthly data")?; - let rtk_monthly = tracker - .get_by_month() - .context("Failed to load monthly token savings from database")?; - let periods = merge_monthly(cc_monthly, rtk_monthly); - - if periods.is_empty() { - println!("No data available. Run some rtk commands to start tracking."); - return Ok(()); - } - - let totals = compute_totals(&periods); - - println!("[cost] Claude Code Economics"); - println!("════════════════════════════════════════════════════"); - println!(); - - println!( - " Spent (ccusage): {}", - format_usd(totals.cc_cost) - ); - println!(" Token breakdown:"); - println!( - " Input: {}", - format_tokens(totals.cc_input_tokens as usize) - ); - println!( - " Output: {}", - format_tokens(totals.cc_output_tokens as usize) - ); - println!( - " Cache writes: {}", - format_tokens(totals.cc_cache_create_tokens as usize) - ); - println!( - " Cache reads: {}", - format_tokens(totals.cc_cache_read_tokens as usize) - ); - println!(); - - println!(" RTK commands: {}", totals.rtk_commands); - println!( - " Tokens saved: {}", - format_tokens(totals.rtk_saved_tokens) - ); - println!(); - - println!(" Estimated Savings:"); - println!(" ┌─────────────────────────────────────────────────┐"); - - if let Some(weighted_savings) = totals.savings_weighted { - let weighted_pct = if totals.cc_cost > 0.0 { - (weighted_savings / totals.cc_cost) * 100.0 - } else { - 0.0 - }; - println!( - " │ Input token pricing: {} ({:.1}%) │", - format_usd(weighted_savings).trim_end(), - weighted_pct - ); - if let Some(input_cpt) = totals.weighted_input_cpt { - println!( - " │ Derived input CPT: {} │", - format_cpt(input_cpt) - ); - } - } else { - println!(" │ Input token pricing: — │"); - } - - println!(" └─────────────────────────────────────────────────┘"); - println!(); - - println!(" How it works:"); - println!(" RTK compresses CLI outputs before they enter Claude's context."); - println!(" Savings derived using API price ratios (out=5x, cache_w=1.25x, cache_r=0.1x)."); - println!(); - - // Verbose mode: legacy metrics - if verbose > 0 { - println!(" Legacy metrics (reference only):"); - if let Some(active_savings) = totals.savings_active { - let active_pct = if totals.cc_cost > 0.0 { - (active_savings / totals.cc_cost) * 100.0 - } else { - 0.0 - }; - println!( - " Active (OVERESTIMATES): {} ({:.1}%)", - format_usd(active_savings), - active_pct - ); - } - if let Some(blended_savings) = totals.savings_blended { - let blended_pct = if totals.cc_cost > 0.0 { - (blended_savings / totals.cc_cost) * 100.0 - } else { - 0.0 - }; - println!( - " Blended (UNDERESTIMATES): {} ({:.2}%)", - format_usd(blended_savings), - blended_pct - ); - } - println!(" Note: Saved tokens estimated via chars/4 heuristic, not exact tokenizer."); - println!(); - } - - Ok(()) -} - -fn display_daily(tracker: &Tracker, verbose: u8) -> Result<()> { - let cc_daily = - ccusage::fetch(Granularity::Daily).context("Failed to fetch ccusage daily data")?; - let rtk_daily = tracker - .get_all_days() - .context("Failed to load daily token savings from database")?; - let periods = merge_daily(cc_daily, rtk_daily); - - println!("Daily Economics"); - println!("════════════════════════════════════════════════════"); - print_period_table(&periods, verbose); - Ok(()) -} - -fn display_weekly(tracker: &Tracker, verbose: u8) -> Result<()> { - let cc_weekly = - ccusage::fetch(Granularity::Weekly).context("Failed to fetch ccusage weekly data")?; - let rtk_weekly = tracker - .get_by_week() - .context("Failed to load weekly token savings from database")?; - let periods = merge_weekly(cc_weekly, rtk_weekly); - - println!("Weekly Economics"); - println!("════════════════════════════════════════════════════"); - print_period_table(&periods, verbose); - Ok(()) -} - -fn display_monthly(tracker: &Tracker, verbose: u8) -> Result<()> { - let cc_monthly = - ccusage::fetch(Granularity::Monthly).context("Failed to fetch ccusage monthly data")?; - let rtk_monthly = tracker - .get_by_month() - .context("Failed to load monthly token savings from database")?; - let periods = merge_monthly(cc_monthly, rtk_monthly); - - println!("Monthly Economics"); - println!("════════════════════════════════════════════════════"); - print_period_table(&periods, verbose); - Ok(()) -} - -fn print_period_table(periods: &[PeriodEconomics], verbose: u8) { - println!(); - - if verbose > 0 { - // Verbose: include legacy metrics - println!( - "{:<12} {:>10} {:>10} {:>10} {:>10} {:>12} {:>12}", - "Period", "Spent", "Saved", "Savings", "Active$", "Blended$", "RTK Cmds" - ); - println!( - "{:-<12} {:-<10} {:-<10} {:-<10} {:-<10} {:-<12} {:-<12}", - "", "", "", "", "", "", "" - ); - - for p in periods { - let spent = p.cc_cost.map(format_usd).unwrap_or_else(|| "—".to_string()); - let saved = p - .rtk_saved_tokens - .map(format_tokens) - .unwrap_or_else(|| "—".to_string()); - let weighted = p - .savings_weighted - .map(format_usd) - .unwrap_or_else(|| "—".to_string()); - let active = p - .savings_active - .map(format_usd) - .unwrap_or_else(|| "—".to_string()); - let blended = p - .savings_blended - .map(format_usd) - .unwrap_or_else(|| "—".to_string()); - let cmds = p - .rtk_commands - .map(|c| c.to_string()) - .unwrap_or_else(|| "—".to_string()); - - println!( - "{:<12} {:>10} {:>10} {:>10} {:>10} {:>12} {:>12}", - p.label, spent, saved, weighted, active, blended, cmds - ); - } - } else { - // Default: single Savings column - println!( - "{:<12} {:>10} {:>10} {:>10} {:>12}", - "Period", "Spent", "Saved", "Savings", "RTK Cmds" - ); - println!( - "{:-<12} {:-<10} {:-<10} {:-<10} {:-<12}", - "", "", "", "", "" - ); - - for p in periods { - let spent = p.cc_cost.map(format_usd).unwrap_or_else(|| "—".to_string()); - let saved = p - .rtk_saved_tokens - .map(format_tokens) - .unwrap_or_else(|| "—".to_string()); - let weighted = p - .savings_weighted - .map(format_usd) - .unwrap_or_else(|| "—".to_string()); - let cmds = p - .rtk_commands - .map(|c| c.to_string()) - .unwrap_or_else(|| "—".to_string()); - - println!( - "{:<12} {:>10} {:>10} {:>10} {:>12}", - p.label, spent, saved, weighted, cmds - ); - } - } - println!(); -} - -// ── Export ── - -fn export_json( - tracker: &Tracker, - daily: bool, - weekly: bool, - monthly: bool, - all: bool, -) -> Result<()> { - #[derive(Serialize)] - struct Export { - daily: Option>, - weekly: Option>, - monthly: Option>, - totals: Option, - } - - let mut export = Export { - daily: None, - weekly: None, - monthly: None, - totals: None, - }; - - if all || daily { - let cc = ccusage::fetch(Granularity::Daily) - .context("Failed to fetch ccusage daily data for JSON export")?; - let rtk = tracker - .get_all_days() - .context("Failed to load daily token savings for JSON export")?; - export.daily = Some(merge_daily(cc, rtk)); - } - - if all || weekly { - let cc = ccusage::fetch(Granularity::Weekly) - .context("Failed to fetch ccusage weekly data for export")?; - let rtk = tracker - .get_by_week() - .context("Failed to load weekly token savings for export")?; - export.weekly = Some(merge_weekly(cc, rtk)); - } - - if all || monthly { - let cc = ccusage::fetch(Granularity::Monthly) - .context("Failed to fetch ccusage monthly data for export")?; - let rtk = tracker - .get_by_month() - .context("Failed to load monthly token savings for export")?; - let periods = merge_monthly(cc, rtk); - export.totals = Some(compute_totals(&periods)); - export.monthly = Some(periods); - } - - println!( - "{}", - serde_json::to_string_pretty(&export) - .context("Failed to serialize economics data to JSON")? - ); - Ok(()) -} - -fn export_csv( - tracker: &Tracker, - daily: bool, - weekly: bool, - monthly: bool, - all: bool, -) -> Result<()> { - // Header (new columns: input_tokens, output_tokens, cache_create, cache_read, weighted_savings) - println!("period,spent,input_tokens,output_tokens,cache_create,cache_read,active_tokens,total_tokens,saved_tokens,weighted_savings,active_savings,blended_savings,rtk_commands"); - - if all || daily { - let cc = ccusage::fetch(Granularity::Daily) - .context("Failed to fetch ccusage daily data for JSON export")?; - let rtk = tracker - .get_all_days() - .context("Failed to load daily token savings for JSON export")?; - let periods = merge_daily(cc, rtk); - for p in periods { - print_csv_row(&p); - } - } - - if all || weekly { - let cc = ccusage::fetch(Granularity::Weekly) - .context("Failed to fetch ccusage weekly data for export")?; - let rtk = tracker - .get_by_week() - .context("Failed to load weekly token savings for export")?; - let periods = merge_weekly(cc, rtk); - for p in periods { - print_csv_row(&p); - } - } - - if all || monthly { - let cc = ccusage::fetch(Granularity::Monthly) - .context("Failed to fetch ccusage monthly data for export")?; - let rtk = tracker - .get_by_month() - .context("Failed to load monthly token savings for export")?; - let periods = merge_monthly(cc, rtk); - for p in periods { - print_csv_row(&p); - } - } - - Ok(()) -} - -fn print_csv_row(p: &PeriodEconomics) { - let spent = p.cc_cost.map(|c| format!("{:.4}", c)).unwrap_or_default(); - let input_tokens = p.cc_input_tokens.map(|t| t.to_string()).unwrap_or_default(); - let output_tokens = p - .cc_output_tokens - .map(|t| t.to_string()) - .unwrap_or_default(); - let cache_create = p - .cc_cache_create_tokens - .map(|t| t.to_string()) - .unwrap_or_default(); - let cache_read = p - .cc_cache_read_tokens - .map(|t| t.to_string()) - .unwrap_or_default(); - let active_tokens = p - .cc_active_tokens - .map(|t| t.to_string()) - .unwrap_or_default(); - let total_tokens = p.cc_total_tokens.map(|t| t.to_string()).unwrap_or_default(); - let saved_tokens = p - .rtk_saved_tokens - .map(|t| t.to_string()) - .unwrap_or_default(); - let weighted_savings = p - .savings_weighted - .map(|s| format!("{:.4}", s)) - .unwrap_or_default(); - let active_savings = p - .savings_active - .map(|s| format!("{:.4}", s)) - .unwrap_or_default(); - let blended_savings = p - .savings_blended - .map(|s| format!("{:.4}", s)) - .unwrap_or_default(); - let cmds = p.rtk_commands.map(|c| c.to_string()).unwrap_or_default(); - - println!( - "{},{},{},{},{},{},{},{},{},{},{},{},{}", - p.label, - spent, - input_tokens, - output_tokens, - cache_create, - cache_read, - active_tokens, - total_tokens, - saved_tokens, - weighted_savings, - active_savings, - blended_savings, - cmds - ); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_convert_saturday_to_monday() { - // Saturday Jan 18 -> Monday Jan 20 - assert_eq!( - convert_saturday_to_monday("2026-01-18"), - Some("2026-01-20".to_string()) - ); - - // Invalid format - assert_eq!(convert_saturday_to_monday("invalid"), None); - } - - #[test] - fn test_period_economics_new() { - let p = PeriodEconomics::new("2026-01"); - assert_eq!(p.label, "2026-01"); - assert!(p.cc_cost.is_none()); - assert!(p.rtk_commands.is_none()); - } - - #[test] - fn test_compute_dual_metrics_with_data() { - let mut p = PeriodEconomics { - label: "2026-01".to_string(), - cc_cost: Some(100.0), - cc_total_tokens: Some(1_000_000), - cc_active_tokens: Some(10_000), - rtk_saved_tokens: Some(5_000), - ..PeriodEconomics::new("2026-01") - }; - - p.compute_dual_metrics(); - - assert!(p.blended_cpt.is_some()); - assert_eq!(p.blended_cpt.unwrap(), 100.0 / 1_000_000.0); - - assert!(p.active_cpt.is_some()); - assert_eq!(p.active_cpt.unwrap(), 100.0 / 10_000.0); - - assert!(p.savings_blended.is_some()); - assert!(p.savings_active.is_some()); - } - - #[test] - fn test_compute_dual_metrics_zero_tokens() { - let mut p = PeriodEconomics { - label: "2026-01".to_string(), - cc_cost: Some(100.0), - cc_total_tokens: Some(0), - cc_active_tokens: Some(0), - rtk_saved_tokens: Some(5_000), - ..PeriodEconomics::new("2026-01") - }; - - p.compute_dual_metrics(); - - assert!(p.blended_cpt.is_none()); - assert!(p.active_cpt.is_none()); - assert!(p.savings_blended.is_none()); - assert!(p.savings_active.is_none()); - } - - #[test] - fn test_compute_dual_metrics_no_ccusage_data() { - let mut p = PeriodEconomics { - label: "2026-01".to_string(), - rtk_saved_tokens: Some(5_000), - ..PeriodEconomics::new("2026-01") - }; - - p.compute_dual_metrics(); - - assert!(p.blended_cpt.is_none()); - assert!(p.active_cpt.is_none()); - } - - #[test] - fn test_merge_monthly_both_present() { - let cc = vec![CcusagePeriod { - key: "2026-01".to_string(), - metrics: ccusage::CcusageMetrics { - input_tokens: 1000, - output_tokens: 500, - cache_creation_tokens: 100, - cache_read_tokens: 200, - total_tokens: 1800, - total_cost: 12.34, - }, - }]; - - let rtk = vec![MonthStats { - month: "2026-01".to_string(), - commands: 10, - input_tokens: 800, - output_tokens: 400, - saved_tokens: 5000, - savings_pct: 50.0, - total_time_ms: 0, - avg_time_ms: 0, - }]; - - let merged = merge_monthly(Some(cc), rtk); - assert_eq!(merged.len(), 1); - assert_eq!(merged[0].label, "2026-01"); - assert_eq!(merged[0].cc_cost, Some(12.34)); - assert_eq!(merged[0].rtk_commands, Some(10)); - } - - #[test] - fn test_merge_monthly_only_ccusage() { - let cc = vec![CcusagePeriod { - key: "2026-01".to_string(), - metrics: ccusage::CcusageMetrics { - input_tokens: 1000, - output_tokens: 500, - cache_creation_tokens: 100, - cache_read_tokens: 200, - total_tokens: 1800, - total_cost: 12.34, - }, - }]; - - let merged = merge_monthly(Some(cc), vec![]); - assert_eq!(merged.len(), 1); - assert_eq!(merged[0].cc_cost, Some(12.34)); - assert!(merged[0].rtk_commands.is_none()); - } - - #[test] - fn test_merge_monthly_only_rtk() { - let rtk = vec![MonthStats { - month: "2026-01".to_string(), - commands: 10, - input_tokens: 800, - output_tokens: 400, - saved_tokens: 5000, - savings_pct: 50.0, - total_time_ms: 0, - avg_time_ms: 0, - }]; - - let merged = merge_monthly(None, rtk); - assert_eq!(merged.len(), 1); - assert!(merged[0].cc_cost.is_none()); - assert_eq!(merged[0].rtk_commands, Some(10)); - } - - #[test] - fn test_merge_monthly_sorted() { - let rtk = vec![ - MonthStats { - month: "2026-03".to_string(), - commands: 5, - input_tokens: 100, - output_tokens: 50, - saved_tokens: 1000, - savings_pct: 40.0, - total_time_ms: 0, - avg_time_ms: 0, - }, - MonthStats { - month: "2026-01".to_string(), - commands: 10, - input_tokens: 200, - output_tokens: 100, - saved_tokens: 2000, - savings_pct: 60.0, - total_time_ms: 0, - avg_time_ms: 0, - }, - ]; - - let merged = merge_monthly(None, rtk); - assert_eq!(merged.len(), 2); - assert_eq!(merged[0].label, "2026-01"); - assert_eq!(merged[1].label, "2026-03"); - } - - #[test] - fn test_compute_weighted_input_cpt() { - let mut p = PeriodEconomics::new("2026-01"); - p.cc_cost = Some(100.0); - p.cc_input_tokens = Some(1000); - p.cc_output_tokens = Some(500); - p.cc_cache_create_tokens = Some(200); - p.cc_cache_read_tokens = Some(5000); - p.rtk_saved_tokens = Some(10_000); - - p.compute_weighted_metrics(); - - // weighted_units = 1000 + 5*500 + 1.25*200 + 0.1*5000 = 1000 + 2500 + 250 + 500 = 4250 - // input_cpt = 100 / 4250 = 0.0235294... - // savings = 10000 * 0.0235294... = 235.29... - - assert!(p.weighted_input_cpt.is_some()); - let cpt = p.weighted_input_cpt.unwrap(); - assert!((cpt - (100.0 / 4250.0)).abs() < 1e-6); - - assert!(p.savings_weighted.is_some()); - let savings = p.savings_weighted.unwrap(); - assert!((savings - 235.294).abs() < 0.01); - } - - #[test] - fn test_compute_weighted_metrics_zero_tokens() { - let mut p = PeriodEconomics::new("2026-01"); - p.cc_cost = Some(100.0); - p.cc_input_tokens = Some(0); - p.cc_output_tokens = Some(0); - p.cc_cache_create_tokens = Some(0); - p.cc_cache_read_tokens = Some(0); - p.rtk_saved_tokens = Some(5000); - - p.compute_weighted_metrics(); - - assert!(p.weighted_input_cpt.is_none()); - assert!(p.savings_weighted.is_none()); - } - - #[test] - fn test_compute_weighted_metrics_no_cache() { - let mut p = PeriodEconomics::new("2026-01"); - p.cc_cost = Some(60.0); - p.cc_input_tokens = Some(1000); - p.cc_output_tokens = Some(1000); - p.cc_cache_create_tokens = Some(0); - p.cc_cache_read_tokens = Some(0); - p.rtk_saved_tokens = Some(3000); - - p.compute_weighted_metrics(); - - // weighted_units = 1000 + 5*1000 = 6000 - // input_cpt = 60 / 6000 = 0.01 - // savings = 3000 * 0.01 = 30 - - assert!(p.weighted_input_cpt.is_some()); - let cpt = p.weighted_input_cpt.unwrap(); - assert!((cpt - 0.01).abs() < 1e-6); - - assert!(p.savings_weighted.is_some()); - let savings = p.savings_weighted.unwrap(); - assert!((savings - 30.0).abs() < 0.01); - } - - #[test] - fn test_set_ccusage_stores_per_type_tokens() { - let mut p = PeriodEconomics::new("2026-01"); - let metrics = ccusage::CcusageMetrics { - input_tokens: 1000, - output_tokens: 500, - cache_creation_tokens: 200, - cache_read_tokens: 3000, - total_tokens: 4700, - total_cost: 50.0, - }; - - p.set_ccusage(&metrics); - - assert_eq!(p.cc_input_tokens, Some(1000)); - assert_eq!(p.cc_output_tokens, Some(500)); - assert_eq!(p.cc_cache_create_tokens, Some(200)); - assert_eq!(p.cc_cache_read_tokens, Some(3000)); - assert_eq!(p.cc_total_tokens, Some(4700)); - assert_eq!(p.cc_cost, Some(50.0)); - } - - #[test] - fn test_compute_totals() { - let periods = vec![ - PeriodEconomics { - label: "2026-01".to_string(), - cc_cost: Some(100.0), - cc_total_tokens: Some(1_000_000), - cc_active_tokens: Some(10_000), - cc_input_tokens: Some(5000), - cc_output_tokens: Some(5000), - cc_cache_create_tokens: Some(100), - cc_cache_read_tokens: Some(984_900), - rtk_commands: Some(5), - rtk_saved_tokens: Some(2000), - rtk_savings_pct: Some(50.0), - weighted_input_cpt: None, - savings_weighted: None, - blended_cpt: None, - active_cpt: None, - savings_blended: None, - savings_active: None, - }, - PeriodEconomics { - label: "2026-02".to_string(), - cc_cost: Some(200.0), - cc_total_tokens: Some(2_000_000), - cc_active_tokens: Some(20_000), - cc_input_tokens: Some(10_000), - cc_output_tokens: Some(10_000), - cc_cache_create_tokens: Some(200), - cc_cache_read_tokens: Some(1_979_800), - rtk_commands: Some(10), - rtk_saved_tokens: Some(3000), - rtk_savings_pct: Some(60.0), - weighted_input_cpt: None, - savings_weighted: None, - blended_cpt: None, - active_cpt: None, - savings_blended: None, - savings_active: None, - }, - ]; - - let totals = compute_totals(&periods); - assert_eq!(totals.cc_cost, 300.0); - assert_eq!(totals.cc_total_tokens, 3_000_000); - assert_eq!(totals.cc_active_tokens, 30_000); - assert_eq!(totals.cc_input_tokens, 15_000); - assert_eq!(totals.cc_output_tokens, 15_000); - assert_eq!(totals.rtk_commands, 15); - assert_eq!(totals.rtk_saved_tokens, 5000); - assert_eq!(totals.rtk_avg_savings_pct, 55.0); - - assert!(totals.weighted_input_cpt.is_some()); - assert!(totals.savings_weighted.is_some()); - assert!(totals.blended_cpt.is_some()); - assert!(totals.active_cpt.is_some()); - } -} diff --git a/src/analytics/ccusage.rs b/src/analytics/ccusage.rs deleted file mode 100644 index c291615b7..000000000 --- a/src/analytics/ccusage.rs +++ /dev/null @@ -1,325 +0,0 @@ -//! Parses Claude Code spending data for economics reporting. -//! -//! Provides isolated interface to ccusage (npm package) for fetching -//! Claude Code API usage metrics. Handles subprocess execution, JSON parsing, -//! and graceful degradation when ccusage is unavailable. - -use crate::core::stream::exec_capture; -use crate::core::utils::{resolved_command, tool_exists}; -use anyhow::{Context, Result}; -use serde::Deserialize; -use std::process::Command; - -// ── Public Types ── - -/// Metrics from ccusage for a single period (day/week/month) -#[derive(Debug, Deserialize)] -pub struct CcusageMetrics { - #[serde(rename = "inputTokens")] - pub input_tokens: u64, - #[serde(rename = "outputTokens")] - pub output_tokens: u64, - #[serde(rename = "cacheCreationTokens", default)] - pub cache_creation_tokens: u64, - #[serde(rename = "cacheReadTokens", default)] - pub cache_read_tokens: u64, - #[serde(rename = "totalTokens")] - pub total_tokens: u64, - #[serde(rename = "totalCost")] - pub total_cost: f64, -} - -/// Period data with key (date/month/week) and metrics -#[derive(Debug)] -pub struct CcusagePeriod { - pub key: String, // "2026-01-30" (daily), "2026-01" (monthly), "2026-01-20" (weekly ISO monday) - pub metrics: CcusageMetrics, -} - -/// Time granularity for ccusage reports -#[derive(Debug, Clone, Copy)] -pub enum Granularity { - Daily, - Weekly, - Monthly, -} - -// ── Internal Types for JSON Deserialization ── - -#[derive(Debug, Deserialize)] -struct DailyResponse { - daily: Vec, -} - -#[derive(Debug, Deserialize)] -struct DailyEntry { - date: String, - #[serde(flatten)] - metrics: CcusageMetrics, -} - -#[derive(Debug, Deserialize)] -struct WeeklyResponse { - weekly: Vec, -} - -#[derive(Debug, Deserialize)] -struct WeeklyEntry { - week: String, // ISO week start (Monday) - #[serde(flatten)] - metrics: CcusageMetrics, -} - -#[derive(Debug, Deserialize)] -struct MonthlyResponse { - monthly: Vec, -} - -#[derive(Debug, Deserialize)] -struct MonthlyEntry { - month: String, - #[serde(flatten)] - metrics: CcusageMetrics, -} - -// ── Public API ── - -/// Check if ccusage binary exists in PATH -fn binary_exists() -> bool { - tool_exists("ccusage") -} - -/// Build the ccusage command, falling back to npx if binary not in PATH -fn build_command() -> Option { - if binary_exists() { - return Some(resolved_command("ccusage")); - } - - // Fallback: try npx - eprintln!("[info] ccusage not installed globally, fetching via npx..."); - let npx_check = resolved_command("npx") - .arg("--yes") - .arg("ccusage") - .arg("--help") - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status(); - - if npx_check.map(|s| s.success()).unwrap_or(false) { - let mut cmd = resolved_command("npx"); - cmd.arg("--yes"); - cmd.arg("ccusage"); - return Some(cmd); - } - - None -} - -/// Fetch usage data from ccusage for the last 90 days -/// -/// Returns `Ok(None)` if ccusage is unavailable (graceful degradation) -/// Returns `Ok(Some(vec))` with parsed data on success -/// Returns `Err` only on unexpected failures (JSON parse, etc.) -pub fn fetch(granularity: Granularity) -> Result>> { - let mut cmd = match build_command() { - Some(cmd) => cmd, - None => { - eprintln!("[warn] ccusage not found. Install: npm i -g ccusage (or use npx ccusage)"); - return Ok(None); - } - }; - - let subcommand = match granularity { - Granularity::Daily => "daily", - Granularity::Weekly => "weekly", - Granularity::Monthly => "monthly", - }; - - cmd.arg(subcommand) - .arg("--json") - .arg("--since") - .arg("20250101"); // 90 days back approx - - let result = match exec_capture(&mut cmd) { - Err(e) => { - eprintln!("[warn] ccusage execution failed: {}", e); - return Ok(None); - } - Ok(r) => r, - }; - - if !result.success() { - eprintln!( - "[warn] ccusage exited with {}: {}", - result.exit_code, - result.stderr.trim() - ); - return Ok(None); - } - - let periods = - parse_json(&result.stdout, granularity).context("Failed to parse ccusage JSON output")?; - - Ok(Some(periods)) -} - -// ── Internal Helpers ── - -fn parse_json(json: &str, granularity: Granularity) -> Result> { - match granularity { - Granularity::Daily => { - let resp: DailyResponse = - serde_json::from_str(json).context("Invalid JSON structure for daily data")?; - Ok(resp - .daily - .into_iter() - .map(|e| CcusagePeriod { - key: e.date, - metrics: e.metrics, - }) - .collect()) - } - Granularity::Weekly => { - let resp: WeeklyResponse = - serde_json::from_str(json).context("Invalid JSON structure for weekly data")?; - Ok(resp - .weekly - .into_iter() - .map(|e| CcusagePeriod { - key: e.week, - metrics: e.metrics, - }) - .collect()) - } - Granularity::Monthly => { - let resp: MonthlyResponse = - serde_json::from_str(json).context("Invalid JSON structure for monthly data")?; - Ok(resp - .monthly - .into_iter() - .map(|e| CcusagePeriod { - key: e.month, - metrics: e.metrics, - }) - .collect()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_monthly_valid() { - let json = r#"{ - "monthly": [ - { - "month": "2026-01", - "inputTokens": 1000, - "outputTokens": 500, - "cacheCreationTokens": 100, - "cacheReadTokens": 200, - "totalTokens": 1800, - "totalCost": 12.34 - } - ] - }"#; - - let result = parse_json(json, Granularity::Monthly); - assert!(result.is_ok()); - let periods = result.unwrap(); - assert_eq!(periods.len(), 1); - assert_eq!(periods[0].key, "2026-01"); - assert_eq!(periods[0].metrics.input_tokens, 1000); - assert_eq!(periods[0].metrics.total_cost, 12.34); - } - - #[test] - fn test_parse_daily_valid() { - let json = r#"{ - "daily": [ - { - "date": "2026-01-30", - "inputTokens": 100, - "outputTokens": 50, - "cacheCreationTokens": 0, - "cacheReadTokens": 0, - "totalTokens": 150, - "totalCost": 0.15 - } - ] - }"#; - - let result = parse_json(json, Granularity::Daily); - assert!(result.is_ok()); - let periods = result.unwrap(); - assert_eq!(periods.len(), 1); - assert_eq!(periods[0].key, "2026-01-30"); - } - - #[test] - fn test_parse_weekly_valid() { - let json = r#"{ - "weekly": [ - { - "week": "2026-01-20", - "inputTokens": 500, - "outputTokens": 250, - "cacheCreationTokens": 50, - "cacheReadTokens": 100, - "totalTokens": 900, - "totalCost": 5.67 - } - ] - }"#; - - let result = parse_json(json, Granularity::Weekly); - assert!(result.is_ok()); - let periods = result.unwrap(); - assert_eq!(periods.len(), 1); - assert_eq!(periods[0].key, "2026-01-20"); - } - - #[test] - fn test_parse_malformed_json() { - let json = r#"{ "monthly": [ { "broken": }"#; - let result = parse_json(json, Granularity::Monthly); - assert!(result.is_err()); - } - - #[test] - fn test_parse_missing_required_fields() { - let json = r#"{ - "monthly": [ - { - "month": "2026-01", - "inputTokens": 100 - } - ] - }"#; - let result = parse_json(json, Granularity::Monthly); - assert!(result.is_err()); // Missing required fields like totalTokens - } - - #[test] - fn test_parse_default_cache_fields() { - let json = r#"{ - "monthly": [ - { - "month": "2026-01", - "inputTokens": 100, - "outputTokens": 50, - "totalTokens": 150, - "totalCost": 1.0 - } - ] - }"#; - - let result = parse_json(json, Granularity::Monthly); - assert!(result.is_ok()); - let periods = result.unwrap(); - assert_eq!(periods[0].metrics.cache_creation_tokens, 0); // default - assert_eq!(periods[0].metrics.cache_read_tokens, 0); - } -} diff --git a/src/analytics/gain.rs b/src/analytics/gain.rs index 9c8c2630a..72f0feca9 100644 --- a/src/analytics/gain.rs +++ b/src/analytics/gain.rs @@ -635,53 +635,8 @@ fn export_csv( Ok(()) } -/// Lightweight scan of recent Claude Code sessions for RTK_DISABLED= overuse. -/// Returns a warning string if bypass rate exceeds 10%, None otherwise. -/// Silently returns None on any error (missing dirs, permission issues, etc.). fn check_rtk_disabled_bypass() -> Option { - use crate::discover::provider::{ClaudeProvider, SessionProvider}; - use crate::discover::registry::has_rtk_disabled_prefix; - - let provider = ClaudeProvider; - - // Quick scan: last 7 days only - let sessions = provider.discover_sessions(None, Some(7)).ok()?; - - // Early bail if no sessions or too many (avoid slow scan) - if sessions.is_empty() || sessions.len() > 200 { - return None; - } - - let mut total_bash: usize = 0; - let mut bypassed: usize = 0; - - for session_path in &sessions { - let extracted = match provider.extract_commands(session_path) { - Ok(cmds) => cmds, - Err(_) => continue, - }; - - for ext_cmd in &extracted { - total_bash += 1; - if has_rtk_disabled_prefix(&ext_cmd.command) { - bypassed += 1; - } - } - } - - if total_bash == 0 { - return None; - } - - let pct = (bypassed as f64 / total_bash as f64) * 100.0; - if pct > 10.0 { - Some(format!( - "[warn] {} commands ({:.0}%) used RTK_DISABLED=1 unnecessarily — run `rtk discover` for details", - bypassed, pct - )) - } else { - None - } + None } fn show_failures(tracker: &Tracker) -> Result<()> { diff --git a/src/analytics/mod.rs b/src/analytics/mod.rs index 109f54d1e..fc1c20c22 100644 --- a/src/analytics/mod.rs +++ b/src/analytics/mod.rs @@ -1,6 +1,3 @@ -//! Token savings analytics and cost reporting. +//! Local-only token savings analytics. -pub mod cc_economics; -pub mod ccusage; pub mod gain; -pub mod session_cmd; diff --git a/src/analytics/session_cmd.rs b/src/analytics/session_cmd.rs deleted file mode 100644 index c36a42c25..000000000 --- a/src/analytics/session_cmd.rs +++ /dev/null @@ -1,440 +0,0 @@ -//! Compares RTK-routed vs raw commands in a coding session. - -use crate::core::utils::format_tokens; -use crate::discover::provider::{ClaudeProvider, ExtractedCommand, SessionProvider}; -use crate::discover::registry::{classify_command, split_command_chain, Classification}; -use anyhow::{Context, Result}; -use std::fs; -use std::path::PathBuf; - -/// A summarized session for display. -struct SessionSummary { - id: String, - date: String, - total_cmds: usize, - rtk_cmds: usize, - output_tokens: usize, -} - -impl SessionSummary { - fn adoption_pct(&self) -> f64 { - if self.total_cmds == 0 { - return 0.0; - } - self.rtk_cmds as f64 / self.total_cmds as f64 * 100.0 - } -} - -/// Count RTK-covered commands from extracted commands. -/// A command is "covered" if it either: -/// - starts with "rtk " (explicit rtk invocation), or -/// - would be rewritten by the hook (classify_command returns Supported) -/// -/// Chained commands (e.g. "cd ./path && rtk ls") are split so each part -/// is classified independently — matching the discover module's behavior. -fn count_rtk_commands(cmds: &[ExtractedCommand]) -> (usize, usize, usize) { - let mut total: usize = 0; - let mut rtk: usize = 0; - for c in cmds { - let parts = split_command_chain(&c.command); - for part in &parts { - total += 1; - if part.starts_with("rtk ") - || matches!(classify_command(part), Classification::Supported { .. }) - { - rtk += 1; - } - } - } - let output: usize = cmds.iter().filter_map(|c| c.output_len).sum(); - (total, rtk, output) -} - -fn progress_bar(pct: f64, width: usize) -> String { - let filled = ((pct / 100.0) * width as f64).round() as usize; - let empty = width.saturating_sub(filled); - format!("{}{}", "@".repeat(filled), ".".repeat(empty)) -} - -pub fn run(_verbose: u8) -> Result<()> { - let provider = ClaudeProvider; - let sessions = provider - .discover_sessions(None, Some(30)) - .context("Failed to discover Claude Code sessions")?; - - if sessions.is_empty() { - println!("No Claude Code sessions found in the last 30 days."); - println!("Make sure Claude Code has been used at least once."); - return Ok(()); - } - - // Group JSONL files by parent session (ignore subagent files) - let mut session_files: Vec = sessions - .into_iter() - .filter(|p| { - // Skip subagent files — only top-level session JSONL - !p.to_string_lossy().contains("subagents") - }) - .collect(); - - // Sort by mtime desc - session_files.sort_by(|a, b| { - let ma = fs::metadata(a) - .and_then(|m| m.modified()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); - let mb = fs::metadata(b) - .and_then(|m| m.modified()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); - mb.cmp(&ma) - }); - - // Take top 10 - session_files.truncate(10); - - let mut summaries: Vec = Vec::new(); - - for path in &session_files { - let cmds = match provider.extract_commands(path) { - Ok(c) => c, - Err(_) => continue, - }; - - if cmds.is_empty() { - continue; - } - - let (total_cmds, rtk_cmds, output_tokens) = count_rtk_commands(&cmds); - - // Extract session ID from filename - let id = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown"); - let short_id = if id.len() > 8 { &id[..8] } else { id }; - - // Extract date from mtime - let date = fs::metadata(path) - .and_then(|m| m.modified()) - .map(|t| { - let elapsed = std::time::SystemTime::now() - .duration_since(t) - .unwrap_or_default(); - let days = elapsed.as_secs() / 86400; - if days == 0 { - "Today".to_string() - } else if days == 1 { - "Yesterday".to_string() - } else { - format!("{}d ago", days) - } - }) - .unwrap_or_else(|_| "?".to_string()); - - summaries.push(SessionSummary { - id: short_id.to_string(), - date, - total_cmds, - rtk_cmds, - output_tokens, - }); - } - - if summaries.is_empty() { - println!("No sessions with Bash commands found."); - return Ok(()); - } - - // Display table - let header = "RTK Session Overview (last 10)"; - println!("{}", header); - println!("{}", "-".repeat(70)); - println!( - "{:<12} {:<12} {:>5} {:>5} {:>9} {:<7} {:>8}", - "Session", "Date", "Cmds", "RTK", "Adoption", "", "Output" - ); - println!("{}", "-".repeat(70)); - - let mut total_cmds = 0; - let mut total_rtk = 0; - - for s in &summaries { - let pct = s.adoption_pct(); - let bar = progress_bar(pct, 5); - total_cmds += s.total_cmds; - total_rtk += s.rtk_cmds; - - println!( - "{:<12} {:<12} {:>5} {:>5} {:>8.0}% {:<7} {:>8}", - s.id, - s.date, - s.total_cmds, - s.rtk_cmds, - pct, - bar, - format_tokens(s.output_tokens), - ); - } - - println!("{}", "-".repeat(70)); - - let avg_adoption = if total_cmds > 0 { - total_rtk as f64 / total_cmds as f64 * 100.0 - } else { - 0.0 - }; - println!("Average adoption: {:.0}%", avg_adoption); - println!("Tip: Run `rtk discover` to find missed RTK opportunities"); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::discover::provider::ExtractedCommand; - use std::io::Write; - use tempfile::NamedTempFile; - - fn make_cmd(command: &str, output_len: Option) -> ExtractedCommand { - ExtractedCommand { - command: command.to_string(), - output_len, - session_id: "test".to_string(), - output_content: None, - is_error: false, - sequence_index: 0, - } - } - - // --- Progress bar --- - - #[test] - fn test_progress_bar_boundaries() { - assert_eq!(progress_bar(0.0, 5), "....."); - assert_eq!(progress_bar(100.0, 5), "@@@@@"); - assert_eq!(progress_bar(50.0, 5), "@@@.."); - } - - // --- count_rtk_commands: core counting logic --- - - #[test] - fn test_count_all_rtk() { - let cmds = vec![ - make_cmd("rtk git status", Some(200)), - make_cmd("rtk cargo test", Some(5000)), - make_cmd("rtk git log -10", Some(800)), - ]; - let (total, rtk, output) = count_rtk_commands(&cmds); - assert_eq!(total, 3); - assert_eq!(rtk, 3); - assert_eq!(output, 6000); - } - - #[test] - fn test_count_hook_rewritten_commands() { - // Hook rewrites "git status" → "rtk git status" but JSONL logs the original. - // count_rtk_commands should detect these via classify_command. - let cmds = vec![ - make_cmd("git status", Some(500)), - make_cmd("cargo test", Some(3000)), - make_cmd("echo hello", Some(100)), - ]; - let (total, rtk, output) = count_rtk_commands(&cmds); - assert_eq!(total, 3); - // git status + cargo test are supported by RTK, echo is not - assert_eq!(rtk, 2); - assert_eq!(output, 3600); - } - - #[test] - fn test_count_mixed_explicit_and_hook() { - let cmds = vec![ - make_cmd("rtk git status", Some(200)), // explicit rtk - make_cmd("git log -5", Some(1000)), // hook-rewritten (logged as raw) - make_cmd("rtk cargo test", Some(5000)), // explicit rtk - make_cmd("echo hello", None), // not supported - ]; - let (total, rtk, output) = count_rtk_commands(&cmds); - assert_eq!(total, 4); - assert_eq!(rtk, 3); // rtk git status + git log + rtk cargo test - assert_eq!(output, 6200); - } - - #[test] - fn test_count_unsupported_commands_not_counted() { - let cmds = vec![ - make_cmd("echo hello", Some(100)), - make_cmd("mkdir -p /tmp/foo", Some(10)), - make_cmd("cd /tmp", Some(5)), - ]; - let (total, rtk, _) = count_rtk_commands(&cmds); - assert_eq!(total, 3); - assert_eq!(rtk, 0); - } - - #[test] - fn test_count_empty_commands() { - let cmds: Vec = vec![]; - let (total, rtk, output) = count_rtk_commands(&cmds); - assert_eq!(total, 0); - assert_eq!(rtk, 0); - assert_eq!(output, 0); - } - - // --- chained commands --- - - #[test] - fn test_count_chained_commands_split() { - // "cd ./path && rtk ls" is one ExtractedCommand but two logical commands. - // cd is ignored/unsupported, ls is supported → 1 out of 2 covered. - let cmds = vec![make_cmd("cd ./your/app/path && rtk ls", Some(200))]; - let (total, rtk, _) = count_rtk_commands(&cmds); - assert_eq!(total, 2, "chain should split into 2 commands"); - assert_eq!(rtk, 1, "only 'rtk ls' is RTK-covered"); - } - - #[test] - fn test_count_chained_all_supported() { - // Both parts are RTK-supported - let cmds = vec![make_cmd("git status && git log -5", Some(500))]; - let (total, rtk, _) = count_rtk_commands(&cmds); - assert_eq!(total, 2, "chain should split into 2 commands"); - assert_eq!(rtk, 2, "both git commands are RTK-covered"); - } - - #[test] - fn test_count_chained_with_semicolon() { - let cmds = vec![make_cmd("cd /tmp; git status; echo done", Some(100))]; - let (total, rtk, _) = count_rtk_commands(&cmds); - assert_eq!(total, 3, "semicolon chain splits into 3 commands"); - assert_eq!(rtk, 1, "only git status is RTK-covered"); - } - - #[test] - fn test_count_chained_no_false_inflation() { - // Single command should still count as 1 - let cmds = vec![make_cmd("git status", Some(100))]; - let (total, rtk, _) = count_rtk_commands(&cmds); - assert_eq!(total, 1); - assert_eq!(rtk, 1); - } - - // --- adoption_pct --- - - #[test] - fn test_adoption_pct_zero_division() { - let s = SessionSummary { - id: "x".to_string(), - date: "Today".to_string(), - total_cmds: 0, - rtk_cmds: 0, - output_tokens: 0, - }; - assert_eq!(s.adoption_pct(), 0.0); - } - - #[test] - fn test_adoption_pct_75_percent() { - let s = SessionSummary { - id: "x".to_string(), - date: "Today".to_string(), - total_cmds: 20, - rtk_cmds: 15, - output_tokens: 0, - }; - assert_eq!(s.adoption_pct(), 75.0); - } - - // --- End-to-end: parse real JSONL and count --- - - #[test] - fn test_parse_jsonl_session_and_count() { - // Simulate a session with 3 Bash commands: 2 rtk, 1 raw - let jsonl = [ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"rtk git status"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"On branch main"}]}}"#, - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t2","name":"Bash","input":{"command":"git log -5"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t2","content":"commit abc123\ncommit def456"}]}}"#, - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t3","name":"Bash","input":{"command":"rtk cargo test"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t3","content":"test result: ok. 5 passed"}]}}"#, - ]; - - let mut tmp = NamedTempFile::new().expect("create tempfile"); - for line in &jsonl { - writeln!(tmp, "{}", line).expect("write line"); - } - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(tmp.path()).expect("parse JSONL"); - - let (total, rtk, _output) = count_rtk_commands(&cmds); - assert_eq!(total, 3, "should find 3 Bash commands"); - // All 3 are RTK-covered: 2 explicit "rtk ..." + 1 hook-rewritten "git log" - assert_eq!(rtk, 3, "all 3 commands should be RTK-covered"); - } - - #[test] - fn test_parse_jsonl_ignores_non_bash_tools() { - // Read/Grep/Edit tools should NOT be counted - let jsonl = [ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Read","input":{"file_path":"/tmp/foo"}}]}}"#, - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t2","name":"Grep","input":{"pattern":"TODO"}}]}}"#, - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t3","name":"Bash","input":{"command":"rtk git status"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t3","content":"clean"}]}}"#, - ]; - - let mut tmp = NamedTempFile::new().expect("create tempfile"); - for line in &jsonl { - writeln!(tmp, "{}", line).expect("write line"); - } - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(tmp.path()).expect("parse JSONL"); - - let (total, rtk, _) = count_rtk_commands(&cmds); - assert_eq!(total, 1, "only Bash tool should be counted"); - assert_eq!(rtk, 1, "the one Bash command is rtk"); - } - - #[test] - fn test_parse_empty_session() { - // Session with no Bash commands at all - let jsonl = [ - r#"{"type":"user","message":{"role":"user","content":"Hello"}}"#, - r#"{"type":"assistant","message":{"role":"assistant","content":"Hi there!"}}"#, - ]; - - let mut tmp = NamedTempFile::new().expect("create tempfile"); - for line in &jsonl { - writeln!(tmp, "{}", line).expect("write line"); - } - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(tmp.path()).expect("parse JSONL"); - - assert!(cmds.is_empty(), "no Bash commands = empty"); - } - - #[test] - fn test_parse_jsonl_chained_command() { - // Claude often runs "cd ./path && git status" as a single Bash call. - // The adoption metric should split the chain and count each part. - let jsonl = [ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"cd ./your/app/path && rtk ls"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"file1.rs\nfile2.rs"}]}}"#, - ]; - - let mut tmp = NamedTempFile::new().expect("create tempfile"); - for line in &jsonl { - writeln!(tmp, "{}", line).expect("write line"); - } - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(tmp.path()).expect("parse JSONL"); - - assert_eq!(cmds.len(), 1, "one Bash tool call"); - let (total, rtk, _) = count_rtk_commands(&cmds); - assert_eq!(total, 2, "chain splits into cd + rtk ls"); - assert_eq!(rtk, 1, "rtk ls is covered, cd is not"); - } -} diff --git a/src/cmds/README.md b/src/cmds/README.md index 010e9495a..fe9fbecf5 100644 --- a/src/cmds/README.md +++ b/src/cmds/README.md @@ -6,7 +6,7 @@ Owns: all command-specific filter logic, organized by ecosystem (git, rust, js, python, go, dotnet, cloud, system). Cross-ecosystem routing (e.g., `lint_cmd` detecting Python and delegating to `ruff_cmd`) is an intra-component concern. -Does **not** own: the TOML DSL filter engine (that's `core/toml_filter`), hook interception (that's `hooks/`), or analytics dashboards (that's `analytics/`). This component **writes** to the tracking DB; analytics **reads** from it. +Does **not** own: the TOML DSL filter engine (that's `core/toml_filter`), hook interception (that's `hooks/`), or removed analytics dashboards. This fork does not write a tracking database. Boundary rule: a module belongs here if and only if it executes an external command and filters its output. Infrastructure that serves multiple modules without calling external commands belongs in `core/`. diff --git a/src/cmds/dotnet/binlog.rs b/src/cmds/dotnet/binlog.rs index 027f482f0..fbc9455ee 100644 --- a/src/cmds/dotnet/binlog.rs +++ b/src/cmds/dotnet/binlog.rs @@ -683,6 +683,7 @@ pub fn parse_build_from_text(text: &str) -> BuildSummary { issue.message.clone(), ); + #[allow(clippy::collapsible_match)] match captures.name("kind").map(|m| m.as_str()) { Some("error") => { if seen_errors.insert(key) { @@ -1008,6 +1009,7 @@ pub fn parse_restore_issues_from_text(text: &str) -> (Vec, Vec MtpProjectKind { | b"testingplatformdotnettestsupport" ); } - Ok(Event::Text(e)) => { - if inside_mtp_element { - if let Ok(text) = e.unescape() { - if text.trim().eq_ignore_ascii_case("true") { - return MtpProjectKind::VsTestBridge; - } + Ok(Event::Text(e)) if inside_mtp_element => { + if let Ok(text) = e.unescape() { + if text.trim().eq_ignore_ascii_case("true") { + return MtpProjectKind::VsTestBridge; } } } diff --git a/src/cmds/dotnet/dotnet_trx.rs b/src/cmds/dotnet/dotnet_trx.rs index 42275e727..ef94f0089 100644 --- a/src/cmds/dotnet/dotnet_trx.rs +++ b/src/cmds/dotnet/dotnet_trx.rs @@ -253,22 +253,16 @@ fn parse_trx_content(content: &str) -> Option { .unwrap_or_else(|| "unknown".to_string()); } } - b"ErrorInfo" => { - if in_failed_result { - in_error_info = true; - } + b"ErrorInfo" if in_failed_result => { + in_error_info = true; } - b"Message" => { - if in_failed_result && in_error_info { - capture_field = Some(CaptureField::Message); - message_buf.clear(); - } + b"Message" if in_failed_result && in_error_info => { + capture_field = Some(CaptureField::Message); + message_buf.clear(); } - b"StackTrace" => { - if in_failed_result && in_error_info { - capture_field = Some(CaptureField::StackTrace); - stack_buf.clear(); - } + b"StackTrace" if in_failed_result && in_error_info => { + capture_field = Some(CaptureField::StackTrace); + stack_buf.clear(); } _ => {} }, @@ -332,34 +326,32 @@ fn parse_trx_content(content: &str) -> Option { b"ErrorInfo" => { in_error_info = false; } - b"UnitTestResult" => { - if in_failed_result { - let mut details = Vec::new(); + b"UnitTestResult" if in_failed_result => { + let mut details = Vec::new(); - let message = message_buf.trim(); - if !message.is_empty() { - details.push(message.to_string()); - } + let message = message_buf.trim(); + if !message.is_empty() { + details.push(message.to_string()); + } - let stack = stack_buf.trim(); - if !stack.is_empty() { - let stack_lines: Vec<&str> = stack.lines().take(3).collect(); - if !stack_lines.is_empty() { - details.push(stack_lines.join("\n")); - } + let stack = stack_buf.trim(); + if !stack.is_empty() { + let stack_lines: Vec<&str> = stack.lines().take(3).collect(); + if !stack_lines.is_empty() { + details.push(stack_lines.join("\n")); } + } - summary.failed_tests.push(FailedTest { - name: failed_test_name.clone(), - details, - }); + summary.failed_tests.push(FailedTest { + name: failed_test_name.clone(), + details, + }); - in_failed_result = false; - in_error_info = false; - capture_field = None; - message_buf.clear(); - stack_buf.clear(); - } + in_failed_result = false; + in_error_info = false; + capture_field = None; + message_buf.clear(); + stack_buf.clear(); } _ => {} }, diff --git a/src/cmds/go/go_cmd.rs b/src/cmds/go/go_cmd.rs index 4ef99daf1..c12ec684f 100644 --- a/src/cmds/go/go_cmd.rs +++ b/src/cmds/go/go_cmd.rs @@ -329,10 +329,8 @@ pub(crate) fn filter_go_test_json(output: &str) -> String { let pkg_result = packages.entry(package.clone()).or_default(); match event.action.as_str() { - "pass" => { - if event.test.is_some() { - pkg_result.pass += 1; - } + "pass" if event.test.is_some() => { + pkg_result.pass += 1; } "fail" => { if let Some(test) = &event.test { @@ -358,10 +356,8 @@ pub(crate) fn filter_go_test_json(output: &str) -> String { pkg_result.package_failed = true; } } - "skip" => { - if event.test.is_some() { - pkg_result.skip += 1; - } + "skip" if event.test.is_some() => { + pkg_result.skip += 1; } "output" => { if let Some(output_text) = &event.output { diff --git a/src/cmds/go/golangci_cmd.rs b/src/cmds/go/golangci_cmd.rs index cb6ca34b0..384ff1855 100644 --- a/src/cmds/go/golangci_cmd.rs +++ b/src/cmds/go/golangci_cmd.rs @@ -339,7 +339,7 @@ pub(crate) fn filter_golangci_json(output: &str, version: u32) -> String { } let mut file_linter_counts: Vec<_> = file_linters.iter().collect(); - file_linter_counts.sort_by(|a, b| b.1.len().cmp(&a.1.len())); + file_linter_counts.sort_by_key(|b| std::cmp::Reverse(b.1.len())); for (linter, linter_issues) in file_linter_counts.iter().take(3) { result.push_str(&format!(" {} ({})\n", linter, linter_issues.len())); diff --git a/src/cmds/js/lint_cmd.rs b/src/cmds/js/lint_cmd.rs index 21eb528f5..25ca4e113 100644 --- a/src/cmds/js/lint_cmd.rs +++ b/src/cmds/js/lint_cmd.rs @@ -107,17 +107,13 @@ pub fn run(args: &[String], verbose: u8) -> Result { "eslint" => { cmd.arg("-f").arg("json"); } - "ruff" => { + "ruff" if !effective_args.contains(&"--output-format".to_string()) => { // Force JSON output for ruff check - if !effective_args.contains(&"--output-format".to_string()) { - cmd.arg("check").arg("--output-format=json"); - } + cmd.arg("check").arg("--output-format=json"); } - "pylint" => { + "pylint" if !effective_args.contains(&"--output-format".to_string()) => { // Force JSON2 output for pylint - if !effective_args.contains(&"--output-format".to_string()) { - cmd.arg("--output-format=json2"); - } + cmd.arg("--output-format=json2"); } "mypy" => { // mypy uses default text output (no special flags) @@ -263,7 +259,7 @@ fn filter_eslint_json(output: &str) -> String { .filter(|r| !r.messages.is_empty()) .map(|r| (r, r.messages.len())) .collect(); - by_file.sort_by(|a, b| b.1.cmp(&a.1)); + by_file.sort_by_key(|b| std::cmp::Reverse(b.1)); // Build output let mut result = String::new(); diff --git a/src/cmds/js/pnpm_cmd.rs b/src/cmds/js/pnpm_cmd.rs index 1048060f4..cfe9873b5 100644 --- a/src/cmds/js/pnpm_cmd.rs +++ b/src/cmds/js/pnpm_cmd.rs @@ -415,8 +415,8 @@ fn run_install(args: &[String], verbose: u8) -> Result { println!("{}", filtered); timer.track( - &format!("pnpm install"), - &format!("rtk pnpm install"), + "pnpm install", + "rtk pnpm install", &combined, &filtered, ); diff --git a/src/cmds/js/tsc_cmd.rs b/src/cmds/js/tsc_cmd.rs index e87988289..8abd2ee80 100644 --- a/src/cmds/js/tsc_cmd.rs +++ b/src/cmds/js/tsc_cmd.rs @@ -193,7 +193,7 @@ pub(crate) fn filter_tsc_output(output: &str) -> String { // Files sorted by error count (most errors first) let mut files_sorted: Vec<_> = by_file.iter().collect(); - files_sorted.sort_by(|a, b| b.1.len().cmp(&a.1.len())); + files_sorted.sort_by_key(|b| std::cmp::Reverse(b.1.len())); // Show every error per file — no limits for (file, file_errors) in &files_sorted { diff --git a/src/cmds/python/mypy_cmd.rs b/src/cmds/python/mypy_cmd.rs index b0b6a43f3..69bc7173a 100644 --- a/src/cmds/python/mypy_cmd.rs +++ b/src/cmds/python/mypy_cmd.rs @@ -181,7 +181,7 @@ pub fn filter_mypy_output(output: &str) -> String { // Files sorted by error count (most errors first) let mut files_sorted: Vec<_> = by_file.iter().collect(); - files_sorted.sort_by(|a, b| b.1.len().cmp(&a.1.len())); + files_sorted.sort_by_key(|b| std::cmp::Reverse(b.1.len())); for (file, file_errors) in &files_sorted { result.push_str(&format!("{} ({} errors)\n", file, file_errors.len())); diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index e85cb134c..742f167ad 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -1194,7 +1194,7 @@ fn filter_cargo_clippy(output: &str) -> String { // Sort warning rules by frequency let mut rule_counts: Vec<_> = by_rule.iter().collect(); - rule_counts.sort_by(|a, b| b.1.len().cmp(&a.1.len())); + rule_counts.sort_by_key(|b| std::cmp::Reverse(b.1.len())); for (rule, locations) in rule_counts.iter().take(15) { result.push_str(&format!(" {} ({}x)\n", rule, locations.len())); diff --git a/src/cmds/system/format_cmd.rs b/src/cmds/system/format_cmd.rs index e147640ea..3fd019b6c 100644 --- a/src/cmds/system/format_cmd.rs +++ b/src/cmds/system/format_cmd.rs @@ -83,17 +83,13 @@ pub fn run(args: &[String], verbose: u8) -> Result { let user_args = args[start_idx..].to_vec(); match formatter.as_str() { - "black" => { + "black" if !user_args.iter().any(|a| a == "--check" || a == "--diff") => { // Inject --check if not present for check mode - if !user_args.iter().any(|a| a == "--check" || a == "--diff") { - cmd.arg("--check"); - } + cmd.arg("--check"); } - "ruff" => { + "ruff" if user_args.is_empty() || !user_args[0].starts_with("format") => { // Add "format" subcommand if not present - if user_args.is_empty() || !user_args[0].starts_with("format") { - cmd.arg("format"); - } + cmd.arg("format"); } _ => {} } diff --git a/src/core/README.md b/src/core/README.md index ca031a15a..402efca2b 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -1,125 +1,12 @@ -# Core Infrastructure +# core/ -> See also [docs/contributing/TECHNICAL.md](../../docs/contributing/TECHNICAL.md) for the full architecture overview +Shared runtime utilities for RTK commands. -## Scope +Includes: +- config loading +- filter helpers +- command runner +- tee output recovery +- display helpers -Domain-agnostic building blocks with **no knowledge of any specific command, hook, or agent**. If a module references "git", "cargo", "claude", or any external tool by name, it does not belong here. Core is a leaf in the dependency graph — it is consumed by all other components but imports from none of them. - -Owns: configuration loading, token tracking persistence, TOML filter engine, tee output recovery, display formatting, telemetry, and shared utilities. - -Does **not** own: command-specific filtering logic (that's `cmds/`), hook lifecycle management (that's `src/hooks/`), or analytics dashboards (that's `analytics/`). - -## Purpose -Core infrastructure shared by all RTK command modules. Every filter, tracker, and command handler depends on these modules. No inward dependencies — leaf in the dependency graph (no circular imports possible). - -## TOML Filter Pipeline - -The TOML DSL applies 8 stages in order: - -1. **strip_ansi**: Remove ANSI escape codes if enabled -2. **replace**: Line-by-line regex substitutions (chainable, supports backreferences) -3. **match_output**: Short-circuit rules (if output matches pattern, return message; `unless` field prevents swallowing errors) -4. **strip/keep_lines**: Filter lines by regex (mutually exclusive) -5. **truncate_lines_at**: Truncate each line to N chars (unicode-safe) -6. **head/tail_lines**: Keep first N or last N lines (with omit message) -7. **max_lines**: Absolute line cap applied after head/tail -8. **on_empty**: Return message if result is empty after all stages - -Three-tier filter lookup (first match wins): -1. `.rtk/filters.toml` (project-local, requires `rtk trust`) -2. `~/.config/rtk/filters.toml` (user-global) -3. Built-in filters concatenated by `build.rs` at compile time - -## Tracking Database Schema - -```sql -CREATE TABLE commands ( - id INTEGER PRIMARY KEY, - timestamp TEXT, -- UTC ISO8601 - original_cmd TEXT, -- "ls -la" - rtk_cmd TEXT, -- "rtk ls" - project_path TEXT, -- cwd (for project-scoped stats) - input_tokens INTEGER, -- estimated from raw output - output_tokens INTEGER, -- estimated from filtered output - saved_tokens INTEGER, -- input - output - savings_pct REAL, -- (saved / input) * 100 - exec_time_ms INTEGER -- elapsed milliseconds -); - -CREATE TABLE parse_failures ( - id INTEGER PRIMARY KEY, - timestamp TEXT, - raw_command TEXT, - error_message TEXT, - fallback_succeeded INTEGER -- 1=yes, 0=no -); -``` - -Project-scoped queries use GLOB patterns (not LIKE) to avoid `_`/`%` wildcard issues in paths. - -## Config Sections - -```toml -[tracking] -enabled = true -history_days = 90 -database_path = "/custom/path/to/tracking.db" # Optional - -[display] -colors = true -emoji = true -max_width = 120 - -[tee] -enabled = true -mode = "failures" # failures | always | never -max_files = 20 -max_file_size = 1048576 -directory = "/custom/tee/dir" - -[telemetry] -enabled = true - -[hooks] -exclude_commands = ["curl", "playwright"] # Never auto-rewrite these - -[limits] -grep_max_results = 200 -grep_max_per_file = 25 -status_max_files = 15 -status_max_untracked = 10 -passthrough_max_chars = 2000 -``` - -## Shared Utilities (utils.rs) - -Key functions available to all command modules: - -| Function | Purpose | -|----------|---------| -| `truncate(s, max)` | Truncate string with `...` suffix | -| `strip_ansi(text)` | Remove ANSI escape/color codes | -| `resolved_command(name)` | Find command in PATH, returns `Command` | -| `tool_exists(name)` | Check if a CLI tool is available | -| `detect_package_manager()` | Detect pnpm/yarn/npm from lockfiles | -| `package_manager_exec(tool)` | Build `Command` using detected package manager | -| `ruby_exec(tool)` | Auto-detect `bundle exec` when `Gemfile` exists | -| `count_tokens(text)` | Estimate tokens: `ceil(chars / 4.0)` | - -## Consumer Contracts - -Core provides infrastructure that `cmds/` and other components consume. These contracts define expected usage. - -### Tracking (`TimedExecution`) - -Consumers must call `timer.track()` on **all** code paths — success, failure, and fallback. Calling `std::process::exit()` before `track()` loses metrics. The raw string passed to `track()` should include both stdout and stderr to produce accurate savings percentages. - -### Tee (`tee_and_hint`) - -Consumers that parse structured output (JSON, NDJSON, state machines) should call `tee::tee_and_hint()` to save raw output for LLM recovery on failure. Tee must be called before `std::process::exit()`. - -For truncation recovery on **success** (e.g., list truncated at 20 items), use `tee::force_tee_hint()` which bypasses the tee mode check and writes regardless of exit code. This ensures LLMs always have a `[full output: ...]` recovery path instead of burning tokens working around missing data. - -## Adding New Functionality -Place new infrastructure code here if it meets **all** of these criteria: (1) it has no dependencies on command modules or hooks, (2) it is used by two or more other modules, and (3) it provides a general-purpose utility rather than command-specific logic. Follow the existing pattern of lazy-initialized resources (`lazy_static!` for regex, on-demand config loading) to preserve the <10ms startup target. Add `#[cfg(test)] mod tests` with unit tests in the same file. +Telemetry/call-home behavior is removed in this fork. Local tracking powers `rtk gain` only. diff --git a/src/core/config.rs b/src/core/config.rs index 8d3da7275..02ac32e6f 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -16,8 +16,6 @@ pub struct Config { #[serde(default)] pub tee: crate::core::tee::TeeConfig, #[serde(default)] - pub telemetry: TelemetryConfig, - #[serde(default)] pub hooks: HooksConfig, #[serde(default)] pub limits: LimitsConfig, @@ -88,15 +86,6 @@ impl Default for FilterConfig { } } -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct TelemetryConfig { - pub enabled: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub consent_given: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub consent_date: Option, -} - #[derive(Debug, Serialize, Deserialize)] pub struct LimitsConfig { /// Max total grep results to show (default: 200) @@ -216,36 +205,7 @@ history_days = 90 #[test] fn test_old_toml_without_consent_fields() { - let toml = r#" -[telemetry] -enabled = true -"#; - let config: Config = toml::from_str(toml).expect("valid toml"); - assert!(config.telemetry.enabled); - assert!(config.telemetry.consent_given.is_none()); - assert!(config.telemetry.consent_date.is_none()); - } - - #[test] - fn test_telemetry_default_disabled() { - let config = Config::default(); - assert!(!config.telemetry.enabled); - assert!(config.telemetry.consent_given.is_none()); - } - - #[test] - fn test_telemetry_consent_roundtrip() { - let toml = r#" -[telemetry] -enabled = true -consent_given = true -consent_date = "2026-04-10T12:00:00Z" -"#; - let config: Config = toml::from_str(toml).expect("valid toml"); - assert_eq!(config.telemetry.consent_given, Some(true)); - assert_eq!( - config.telemetry.consent_date.as_deref(), - Some("2026-04-10T12:00:00Z") - ); + let toml = r#""#; + let _config: Config = toml::from_str(toml).expect("valid toml"); } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 01317e942..454a7f8a4 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -7,8 +7,6 @@ pub mod filter; pub mod runner; pub mod stream; pub mod tee; -pub mod telemetry; -pub mod telemetry_cmd; pub mod toml_filter; pub mod tracking; pub mod utils; diff --git a/src/core/telemetry.rs b/src/core/telemetry.rs deleted file mode 100644 index 97e057be1..000000000 --- a/src/core/telemetry.rs +++ /dev/null @@ -1,598 +0,0 @@ -//! Optional usage ping so we know which commands people run most. - -use super::constants::RTK_DATA_DIR; -use crate::core::config; -use crate::core::tracking; -use sha2::{Digest, Sha256}; -use std::fmt::Write as FmtWrite; -use std::io::Write as IoWrite; -use std::path::PathBuf; -use std::sync::OnceLock; - -static CACHED_SALT: OnceLock = OnceLock::new(); - -const TELEMETRY_URL: Option<&str> = option_env!("RTK_TELEMETRY_URL"); -const TELEMETRY_TOKEN: Option<&str> = option_env!("RTK_TELEMETRY_TOKEN"); -const PING_INTERVAL_SECS: u64 = 23 * 3600; // 23 hours - -/// Send a telemetry ping if enabled and not already sent today. -/// Fire-and-forget: errors are silently ignored. -pub fn maybe_ping() { - // No URL compiled in → telemetry disabled - if TELEMETRY_URL.is_none() { - return; - } - - // Check opt-out: env var - if std::env::var("RTK_TELEMETRY_DISABLED").unwrap_or_default() == "1" { - return; - } - - // Load config once (avoid double disk read) - let cfg = match config::Config::load() { - Ok(c) => c, - Err(_) => return, - }; - - // RGPD: require explicit consent before any telemetry - match cfg.telemetry.consent_given { - Some(true) => {} - Some(false) | None => return, - } - - // Check opt-out: config.toml - if !cfg.telemetry.enabled { - return; - } - - // Check last ping time - let marker = telemetry_marker_path(); - if let Ok(metadata) = std::fs::metadata(&marker) { - if let Ok(modified) = metadata.modified() { - if let Ok(elapsed) = modified.elapsed() { - if elapsed.as_secs() < PING_INTERVAL_SECS { - return; - } - } - } - } - - // Touch marker file immediately (before sending) to avoid double-ping - touch_marker(&marker); - - // Spawn thread so we never block the CLI - std::thread::spawn(|| { - let _ = send_ping(); - }); -} - -fn send_ping() -> Result<(), Box> { - let url = TELEMETRY_URL.ok_or("no telemetry URL")?; - let device_hash = generate_device_hash(); - let version = env!("CARGO_PKG_VERSION").to_string(); - let os = std::env::consts::OS.to_string(); - let arch = std::env::consts::ARCH.to_string(); - let install_method = detect_install_method(); - - // Get stats from tracking DB (single connection for both basic + enriched) - let tracker = tracking::Tracker::new().ok(); - let (commands_24h, top_commands, savings_pct, tokens_saved_24h, tokens_saved_total) = - match &tracker { - Some(t) => get_stats(t), - None => (0, vec![], None, 0, 0), - }; - let enriched = match &tracker { - Some(t) => get_enriched_stats(t), - None => EnrichedStats { - passthrough_top: vec![], - parse_failures_24h: 0, - low_savings_commands: vec![], - avg_savings_per_command: 0.0, - hook_type: detect_hook_type(), - custom_toml_filters: count_custom_toml_filters(), - first_seen_days: 0, - active_days_30d: 0, - commands_total: 0, - ecosystem_mix: serde_json::json!({}), - tokens_saved_30d: 0, - estimated_savings_usd_30d: 0.0, - has_config_toml: detect_has_config(), - exclude_commands_count: count_exclude_commands(), - projects_count: 0, - meta_usage: serde_json::json!({}), - }, - }; - - let payload = serde_json::json!({ - "device_hash": device_hash, - "version": version, - "os": os, - "arch": arch, - "install_method": install_method, - "commands_24h": commands_24h, - "top_commands": top_commands, - "savings_pct": savings_pct, - "tokens_saved_24h": tokens_saved_24h, - "tokens_saved_total": tokens_saved_total, - // Quality: identify gaps and weak filters - "passthrough_top": enriched.passthrough_top, - "parse_failures_24h": enriched.parse_failures_24h, - "low_savings_commands": enriched.low_savings_commands, - "avg_savings_per_command": enriched.avg_savings_per_command, - // Adoption: which tools and configs - "hook_type": enriched.hook_type, - "custom_toml_filters": enriched.custom_toml_filters, - // Retention: engagement signals - "first_seen_days": enriched.first_seen_days, - "active_days_30d": enriched.active_days_30d, - "commands_total": enriched.commands_total, - // Ecosystem: where to invest filters - "ecosystem_mix": enriched.ecosystem_mix, - // Economics: value delivered - "tokens_saved_30d": enriched.tokens_saved_30d, - "estimated_savings_usd_30d": enriched.estimated_savings_usd_30d, - // Configuration: user maturity - "has_config_toml": enriched.has_config_toml, - "exclude_commands_count": enriched.exclude_commands_count, - "projects_count": enriched.projects_count, - // Meta-commands: feature adoption - "meta_usage": enriched.meta_usage, - }); - - let mut req = ureq::post(url).set("Content-Type", "application/json"); - - if let Some(token) = TELEMETRY_TOKEN { - req = req.set("X-RTK-Token", token); - } - - // 2 second timeout — if server is down, we move on - req.timeout(std::time::Duration::from_secs(2)) - .send_string(&payload.to_string())?; - - Ok(()) -} - -pub fn generate_device_hash() -> String { - let salt = get_or_create_salt(); - let mut hasher = Sha256::new(); - hasher.update(salt.as_bytes()); - format!("{:x}", hasher.finalize()) -} - -fn get_or_create_salt() -> String { - CACHED_SALT - .get_or_init(|| { - let salt_path = salt_file_path(); - - if let Ok(contents) = std::fs::read_to_string(&salt_path) { - let trimmed = contents.trim().to_string(); - if trimmed.len() == 64 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) { - return trimmed; - } - } - - let salt = random_salt(); - if let Some(parent) = salt_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Ok(mut f) = std::fs::File::create(&salt_path) { - let _ = f.write_all(salt.as_bytes()); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions( - &salt_path, - std::fs::Permissions::from_mode(0o600), - ); - } - } - salt - }) - .clone() -} - -fn random_salt() -> String { - let mut buf = [0u8; 32]; - if getrandom::fill(&mut buf).is_err() { - let fallback = format!("{:?}:{}", std::time::SystemTime::now(), std::process::id()); - let mut hasher = Sha256::new(); - hasher.update(fallback.as_bytes()); - return format!("{:x}", hasher.finalize()); - } - buf.iter().fold(String::new(), |mut output, b| { - let _ = write!(output, "{b:02x}"); - output - }) -} - -pub fn salt_file_path() -> PathBuf { - dirs::data_local_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join("rtk") - .join(".device_salt") -} - -fn get_stats(tracker: &tracking::Tracker) -> (i64, Vec, Option, i64, i64) { - let since_24h = chrono::Utc::now() - chrono::Duration::hours(24); - - let commands_24h = tracker.count_commands_since(since_24h).unwrap_or(0); - let top_commands = tracker.top_commands(5).unwrap_or_default(); - let savings_pct = tracker.overall_savings_pct().ok(); - let tokens_saved_24h = tracker.tokens_saved_24h(since_24h).unwrap_or(0); - let tokens_saved_total = tracker.total_tokens_saved().unwrap_or(0); - - ( - commands_24h, - top_commands, - savings_pct, - tokens_saved_24h, - tokens_saved_total, - ) -} - -struct EnrichedStats { - // Quality: identify gaps and weak filters - passthrough_top: Vec, - parse_failures_24h: i64, - low_savings_commands: Vec, - avg_savings_per_command: f64, - // Adoption: which tools and configs - hook_type: String, - custom_toml_filters: usize, - // Retention: engagement signals - first_seen_days: i64, - active_days_30d: i64, - commands_total: i64, - // Ecosystem: where to invest filters - ecosystem_mix: serde_json::Value, - // Economics: value delivered - tokens_saved_30d: i64, - estimated_savings_usd_30d: f64, - // Configuration: user maturity - has_config_toml: bool, - exclude_commands_count: usize, - projects_count: i64, - // Meta-commands: feature adoption - meta_usage: serde_json::Value, -} - -fn get_enriched_stats(tracker: &tracking::Tracker) -> EnrichedStats { - let since_24h = chrono::Utc::now() - chrono::Duration::hours(24); - - let passthrough_top = tracker - .top_passthrough(5) - .unwrap_or_default() - .into_iter() - .map(|(cmd, count)| format!("{}:{}", cmd, count)) - .collect(); - - let parse_failures_24h = tracker.parse_failures_since(since_24h).unwrap_or(0); - - let low_savings_commands = tracker - .low_savings_commands(5) - .unwrap_or_default() - .into_iter() - .map(|(cmd, pct)| format!("{}:{:.0}%", cmd, pct)) - .collect(); - - let avg_savings_per_command = tracker.avg_savings_per_command().unwrap_or(0.0); - - let first_seen_days = tracker.first_seen_days().unwrap_or(0); - let active_days_30d = tracker.active_days_30d().unwrap_or(0); - let commands_total = tracker.commands_total().unwrap_or(0); - - let ecosystem_mix = serde_json::Value::Object( - tracker - .ecosystem_mix() - .unwrap_or_default() - .into_iter() - .map(|(k, v)| (k, serde_json::json!(v))) - .collect(), - ); - - let tokens_saved_30d = tracker.tokens_saved_30d().unwrap_or(0); - // Estimate USD savings: tokens_saved are input tokens (CLI output compressed before - // reaching the LLM). Use input pricing: Claude Sonnet $3/Mtok. - let estimated_savings_usd_30d = tokens_saved_30d as f64 / 1_000_000.0 * 3.0; - - let projects_count = tracker.projects_count().unwrap_or(0); - - let meta_usage = build_meta_usage(tracker); - - EnrichedStats { - passthrough_top, - parse_failures_24h, - low_savings_commands, - avg_savings_per_command, - hook_type: detect_hook_type(), - custom_toml_filters: count_custom_toml_filters(), - first_seen_days, - active_days_30d, - commands_total, - ecosystem_mix, - tokens_saved_30d, - estimated_savings_usd_30d, - projects_count, - has_config_toml: detect_has_config(), - exclude_commands_count: count_exclude_commands(), - meta_usage, - } -} - -/// Build meta-command usage counts (gain, discover, proxy, verify, learn, init). -fn build_meta_usage(tracker: &tracking::Tracker) -> serde_json::Value { - let meta_cmds = ["gain", "discover", "proxy", "verify", "learn", "init"]; - let mut usage = serde_json::Map::new(); - for meta in &meta_cmds { - let count = tracker.count_meta_command(meta).unwrap_or(0); - if count > 0 { - usage.insert(meta.to_string(), serde_json::json!(count)); - } - } - serde_json::Value::Object(usage) -} - -/// Check if user has a config.toml file. -fn detect_has_config() -> bool { - dirs::config_dir() - .map(|d| d.join("rtk/config.toml").exists()) - .unwrap_or(false) -} - -/// Count commands in exclude_commands config. -fn count_exclude_commands() -> usize { - crate::core::config::Config::load() - .map(|c| c.hooks.exclude_commands.len()) - .unwrap_or(0) -} - -/// Detect which AI agent hook is installed. -fn detect_hook_type() -> String { - let home = match dirs::home_dir() { - Some(h) => h, - None => return "unknown".to_string(), - }; - - // Check in order of popularity - let checks = [ - (home.join(".claude/hooks/rtk-rewrite.sh"), "claude"), - (home.join(".claude/hooks/rtk-rewrite.json"), "claude"), - (home.join(".gemini/hooks/rtk-hook.sh"), "gemini"), - (home.join(".codex/AGENTS.md"), "codex"), - (home.join(".cursor/hooks/rtk-rewrite.json"), "cursor"), - ]; - - for (path, name) in &checks { - if path.exists() { - return name.to_string(); - } - } - - // Check project-level hooks - if let Ok(cwd) = std::env::current_dir() { - if cwd.join(".claude/hooks/rtk-rewrite.sh").exists() { - return "claude".to_string(); - } - } - - "none".to_string() -} - -/// Count user-defined TOML filter files (project-local + global). -fn count_custom_toml_filters() -> usize { - let mut count = 0; - - // Project-local: .rtk/filters/*.toml - if let Ok(cwd) = std::env::current_dir() { - if let Ok(entries) = std::fs::read_dir(cwd.join(".rtk/filters")) { - count += entries - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().is_some_and(|ext| ext == "toml")) - .count(); - } - } - - // Global: ~/.config/rtk/filters/*.toml - if let Some(config_dir) = dirs::config_dir() { - if let Ok(entries) = std::fs::read_dir(config_dir.join("rtk/filters")) { - count += entries - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().is_some_and(|ext| ext == "toml")) - .count(); - } - } - - count -} - -fn detect_install_method() -> &'static str { - let exe = match std::env::current_exe() { - Ok(p) => p, - Err(_) => return "unknown", - }; - let real_path = std::fs::canonicalize(&exe) - .unwrap_or(exe) - .to_string_lossy() - .to_string(); - install_method_from_path(&real_path) -} - -fn install_method_from_path(path: &str) -> &'static str { - if path.contains("/Cellar/rtk/") || path.contains("/homebrew/") { - "homebrew" - } else if path.contains("/.cargo/bin/") || path.contains("\\.cargo\\bin\\") { - "cargo" - } else if path.contains("/.local/bin/") || path.contains("\\.local\\bin\\") { - "script" - } else if path.contains("/nix/store/") { - "nix" - } else { - "other" - } -} - -pub fn telemetry_marker_path() -> PathBuf { - let data_dir = dirs::data_local_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join(RTK_DATA_DIR); - let _ = std::fs::create_dir_all(&data_dir); - data_dir.join(".telemetry_last_ping") -} - -fn touch_marker(path: &PathBuf) { - let _ = std::fs::write(path, b""); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_device_hash_is_stable() { - let h1 = generate_device_hash(); - let h2 = generate_device_hash(); - assert_eq!(h1, h2); - assert_eq!(h1.len(), 64); - } - - #[test] - fn test_device_hash_is_valid_hex() { - let hash = generate_device_hash(); - assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn test_salt_is_persisted() { - let s1 = get_or_create_salt(); - let s2 = get_or_create_salt(); - assert_eq!(s1, s2); - assert_eq!(s1.len(), 64); - assert!(s1.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn test_random_salt_uniqueness() { - let s1 = random_salt(); - let s2 = random_salt(); - assert_ne!(s1, s2); - assert_eq!(s1.len(), 64); - assert_eq!(s2.len(), 64); - } - - #[test] - fn test_salt_file_path_is_in_rtk_dir() { - let path = salt_file_path(); - assert!(path.to_string_lossy().contains("rtk")); - assert!(path.to_string_lossy().contains(".device_salt")); - } - - #[test] - fn test_marker_path_exists() { - let path = telemetry_marker_path(); - assert!(path.to_string_lossy().contains("rtk")); - } - - #[test] - fn test_install_method_unix_paths() { - assert_eq!( - install_method_from_path("/opt/homebrew/Cellar/rtk/0.28.0/bin/rtk"), - "homebrew" - ); - assert_eq!( - install_method_from_path("/usr/local/homebrew/bin/rtk"), - "homebrew" - ); - assert_eq!( - install_method_from_path("/home/user/.cargo/bin/rtk"), - "cargo" - ); - assert_eq!( - install_method_from_path("/home/user/.local/bin/rtk"), - "script" - ); - assert_eq!( - install_method_from_path("/nix/store/abc123-rtk/bin/rtk"), - "nix" - ); - assert_eq!(install_method_from_path("/usr/bin/rtk"), "other"); - } - - #[test] - fn test_install_method_windows_paths() { - assert_eq!( - install_method_from_path("C:\\Users\\user\\.cargo\\bin\\rtk.exe"), - "cargo" - ); - assert_eq!( - install_method_from_path("C:\\Users\\user\\.local\\bin\\rtk.exe"), - "script" - ); - assert_eq!( - install_method_from_path("C:\\Program Files\\rtk\\rtk.exe"), - "other" - ); - } - - #[test] - fn test_detect_install_method_returns_known_value() { - let method = detect_install_method(); - assert!( - ["homebrew", "cargo", "script", "nix", "other", "unknown"].contains(&method), - "Unexpected install method: {}", - method - ); - } - - #[test] - fn test_get_stats_returns_tuple() { - let tracker = match tracking::Tracker::new() { - Ok(t) => t, - Err(_) => return, // No DB — skip - }; - let (cmds, top, pct, saved_24h, saved_total) = get_stats(&tracker); - assert!(cmds >= 0); - assert!(top.len() <= 5); - assert!(saved_24h >= 0); - assert!(saved_total >= 0); - if let Some(p) = pct { - assert!((0.0..=100.0).contains(&p)); - } - } - - #[test] - fn test_enriched_stats_returns_valid_data() { - let tracker = match tracking::Tracker::new() { - Ok(t) => t, - Err(_) => return, - }; - let stats = get_enriched_stats(&tracker); - assert!(stats.passthrough_top.len() <= 5); - assert!(stats.parse_failures_24h >= 0); - assert!(stats.low_savings_commands.len() <= 5); - assert!((0.0..=100.0).contains(&stats.avg_savings_per_command)); - assert!( - ["claude", "gemini", "codex", "cursor", "none", "unknown"] - .iter() - .any(|&h| stats.hook_type.starts_with(h)), - "Unexpected hook type: {}", - stats.hook_type - ); - } - - #[test] - fn test_detect_hook_type_returns_known() { - let ht = detect_hook_type(); - assert!( - ["claude", "gemini", "codex", "cursor", "none", "unknown"].contains(&ht.as_str()), - "Unexpected hook type: {}", - ht - ); - } - - #[test] - fn test_count_custom_toml_filters() { - // Should not panic even if directories don't exist - let count = count_custom_toml_filters(); - assert!(count < 10000); // sanity check - } -} diff --git a/src/core/telemetry_cmd.rs b/src/core/telemetry_cmd.rs deleted file mode 100644 index 70ba30db7..000000000 --- a/src/core/telemetry_cmd.rs +++ /dev/null @@ -1,183 +0,0 @@ -use anyhow::{Context, Result}; -use clap::Subcommand; - -#[derive(Debug, Subcommand)] -pub enum TelemetrySubcommand { - Status, - Enable, - Disable, - Forget, -} - -pub fn run(command: &TelemetrySubcommand) -> Result<()> { - match command { - TelemetrySubcommand::Status => run_status(), - TelemetrySubcommand::Enable => run_enable(), - TelemetrySubcommand::Disable => run_disable(), - TelemetrySubcommand::Forget => run_forget(), - } -} - -fn run_status() -> Result<()> { - let config = crate::core::config::Config::load().unwrap_or_default(); - - let consent_str = match config.telemetry.consent_given { - Some(true) => "yes", - Some(false) => "no", - None => "never asked", - }; - - let enabled_str = if config.telemetry.enabled { - "yes" - } else { - "no" - }; - - let env_override = std::env::var("RTK_TELEMETRY_DISABLED").unwrap_or_default() == "1"; - - println!("Telemetry status:"); - println!(" consent: {}", consent_str); - if let Some(date) = &config.telemetry.consent_date { - println!(" consent date: {}", date); - } - println!(" enabled: {}", enabled_str); - if env_override { - println!(" env override: RTK_TELEMETRY_DISABLED=1 (blocked)"); - } - - let salt_path = super::telemetry::salt_file_path(); - if salt_path.exists() { - let hash = super::telemetry::generate_device_hash(); - println!(" device hash: {}...{}", &hash[..8], &hash[56..]); - } else { - println!(" device hash: (no salt file)"); - } - - println!(); - println!("Data controller: RTK AI Labs, contact@rtk-ai.app"); - println!("Details: https://github.com/rtk-ai/rtk/blob/main/docs/TELEMETRY.md"); - - Ok(()) -} - -fn run_enable() -> Result<()> { - use std::io::{self, BufRead, IsTerminal}; - - if !io::stdin().is_terminal() { - anyhow::bail!( - "consent requires interactive terminal — cannot enable telemetry in piped mode" - ); - } - - eprintln!("RTK collects anonymous usage metrics once per day to improve filters."); - eprintln!(); - eprintln!(" What: command names (not arguments), token savings, OS, version"); - eprintln!(" Who: RTK AI Labs, contact@rtk-ai.app"); - eprintln!(" Details: https://github.com/rtk-ai/rtk/blob/main/docs/TELEMETRY.md"); - eprintln!(); - eprint!("Enable anonymous telemetry? [y/N] "); - - let stdin = io::stdin(); - let mut line = String::new(); - stdin - .lock() - .read_line(&mut line) - .context("Failed to read user input")?; - - let accepted = { - let response = line.trim().to_lowercase(); - response == "y" || response == "yes" - }; - - crate::hooks::init::save_telemetry_consent(accepted)?; - - if accepted { - println!("Telemetry enabled. Disable anytime: rtk telemetry disable"); - } else { - println!("Telemetry not enabled."); - } - - Ok(()) -} - -fn run_disable() -> Result<()> { - crate::hooks::init::save_telemetry_consent(false)?; - println!("Telemetry disabled."); - Ok(()) -} - -fn run_forget() -> Result<()> { - crate::hooks::init::save_telemetry_consent(false)?; - - let salt_path = super::telemetry::salt_file_path(); - let marker_path = super::telemetry::telemetry_marker_path(); - - // Compute device hash before deleting the salt - let device_hash = if salt_path.exists() { - Some(super::telemetry::generate_device_hash()) - } else { - None - }; - - if salt_path.exists() { - std::fs::remove_file(&salt_path) - .with_context(|| format!("Failed to delete {}", salt_path.display()))?; - } - - if marker_path.exists() { - let _ = std::fs::remove_file(&marker_path); - } - - // Purge local tracking database (GDPR Art. 17 — right to erasure applies to local data too) - let db_path = dirs::data_local_dir() - .unwrap_or_else(|| std::path::PathBuf::from(".")) - .join(super::constants::RTK_DATA_DIR) - .join(super::constants::HISTORY_DB); - if db_path.exists() { - match std::fs::remove_file(&db_path) { - Ok(()) => println!("Local tracking database deleted: {}", db_path.display()), - Err(e) => eprintln!("rtk: could not delete {}: {}", db_path.display(), e), - } - } - - // Send server-side erasure request - if let Some(hash) = device_hash { - match send_erasure_request(&hash) { - Ok(()) => { - println!("Erasure request sent to server."); - } - Err(e) => { - eprintln!("rtk: could not reach server: {}", e); - eprintln!(" To complete erasure, email contact@rtk-ai.app"); - eprintln!(" with your device hash: {}", hash); - } - } - } - - println!("Local telemetry data deleted. Telemetry disabled."); - Ok(()) -} - -fn send_erasure_request(device_hash: &str) -> Result<()> { - let url = option_env!("RTK_TELEMETRY_URL"); - let url = match url { - Some(u) => format!("{}/erasure", u), - None => anyhow::bail!("no telemetry endpoint configured"), - }; - - let payload = serde_json::json!({ - "device_hash": device_hash, - "action": "erasure", - }); - - let mut req = ureq::post(&url).set("Content-Type", "application/json"); - - if let Some(token) = option_env!("RTK_TELEMETRY_TOKEN") { - req = req.set("X-RTK-Token", token); - } - - req.timeout(std::time::Duration::from_secs(5)) - .send_string(&payload.to_string())?; - - Ok(()) -} diff --git a/src/core/toml_filter.rs b/src/core/toml_filter.rs index 06060d22d..46221f698 100644 --- a/src/core/toml_filter.rs +++ b/src/core/toml_filter.rs @@ -286,7 +286,6 @@ const RUST_HANDLED_COMMANDS: &[&str] = &[ "init", "wget", "wc", - "gain", "config", "vitest", "prisma", @@ -300,7 +299,6 @@ const RUST_HANDLED_COMMANDS: &[&str] = &[ "npm", "npx", "curl", - "discover", "ruff", "pytest", "mypy", diff --git a/src/core/tracking.rs b/src/core/tracking.rs index 359e4f688..289091a99 100644 --- a/src/core/tracking.rs +++ b/src/core/tracking.rs @@ -700,10 +700,6 @@ impl Tracker { /// } /// # Ok::<(), anyhow::Error>(()) /// ``` - pub fn get_all_days(&self) -> Result> { - self.get_all_days_filtered(None) // delegate to filtered variant - } - /// Get daily statistics filtered by project path. // added pub fn get_all_days_filtered(&self, project_path: Option<&str>) -> Result> { let (project_exact, project_glob) = project_filter_params(project_path); // added @@ -773,6 +769,7 @@ impl Tracker { /// } /// # Ok::<(), anyhow::Error>(()) /// ``` + #[allow(dead_code)] pub fn get_by_week(&self) -> Result> { self.get_by_week_filtered(None) // delegate to filtered variant } @@ -848,6 +845,7 @@ impl Tracker { /// } /// # Ok::<(), anyhow::Error>(()) /// ``` + #[allow(dead_code)] pub fn get_by_month(&self) -> Result> { self.get_by_month_filtered(None) // delegate to filtered variant } @@ -960,261 +958,6 @@ impl Tracker { Ok(rows.collect::, _>>()?) } - - /// Count commands since a given timestamp (for telemetry). - pub fn count_commands_since(&self, since: chrono::DateTime) -> Result { - let ts = since.format("%Y-%m-%dT%H:%M:%S").to_string(); - let count: i64 = self.conn.query_row( - "SELECT COUNT(*) FROM commands WHERE timestamp >= ?1", - params![ts], - |row| row.get(0), - )?; - Ok(count) - } - - /// Get top N commands by frequency (for telemetry). - pub fn top_commands(&self, limit: usize) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT rtk_cmd, COUNT(*) as cnt FROM commands - GROUP BY rtk_cmd ORDER BY cnt DESC LIMIT ?1", - )?; - let rows = stmt.query_map(params![limit as i64], |row| { - let cmd: String = row.get(0)?; - // Extract just the command name (e.g. "rtk git status" → "git") - Ok(cmd.split_whitespace().nth(1).unwrap_or(&cmd).to_string()) - })?; - Ok(rows.filter_map(|r| r.ok()).collect()) - } - - /// Get overall savings percentage (for telemetry). - pub fn overall_savings_pct(&self) -> Result { - let (total_input, total_saved): (i64, i64) = self.conn.query_row( - "SELECT COALESCE(SUM(input_tokens), 0), COALESCE(SUM(saved_tokens), 0) FROM commands", - [], - |row| Ok((row.get(0)?, row.get(1)?)), - )?; - if total_input > 0 { - Ok((total_saved as f64 / total_input as f64) * 100.0) - } else { - Ok(0.0) - } - } - - /// Get total tokens saved across all tracked commands (for telemetry). - pub fn total_tokens_saved(&self) -> Result { - let saved: i64 = self.conn.query_row( - "SELECT COALESCE(SUM(saved_tokens), 0) FROM commands", - [], - |row| row.get(0), - )?; - Ok(saved) - } - - /// Get tokens saved in the last 24 hours (for telemetry). - pub fn tokens_saved_24h(&self, since: chrono::DateTime) -> Result { - let ts = since.format("%Y-%m-%dT%H:%M:%S").to_string(); - let saved: i64 = self.conn.query_row( - "SELECT COALESCE(SUM(saved_tokens), 0) FROM commands WHERE timestamp >= ?1", - params![ts], - |row| row.get(0), - )?; - Ok(saved) - } - - /// Top N passthrough commands (0% savings) — commands missing a filter. - /// Groups by first word only to avoid leaking arguments into telemetry. - pub fn top_passthrough(&self, limit: usize) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT TRIM(SUBSTR(original_cmd, 1, INSTR(original_cmd || ' ', ' ') - 1)) as tool, - COUNT(*) as cnt FROM commands - WHERE input_tokens = 0 AND output_tokens = 0 - GROUP BY tool ORDER BY cnt DESC LIMIT ?1", - )?; - let rows = stmt.query_map(params![limit as i64], |row| { - let cmd: String = row.get(0)?; - let count: i64 = row.get(1)?; - Ok((cmd, count)) - })?; - Ok(rows.filter_map(|r| r.ok()).collect()) - } - - /// Count parse failures in the last 24 hours. - pub fn parse_failures_since(&self, since: chrono::DateTime) -> Result { - let ts = since.format("%Y-%m-%dT%H:%M:%S").to_string(); - let count: i64 = self.conn.query_row( - "SELECT COUNT(*) FROM parse_failures WHERE timestamp >= ?1", - params![ts], - |row| row.get(0), - )?; - Ok(count) - } - - /// Count commands with low savings (<30%) — filters that need improvement. - pub fn low_savings_commands(&self, limit: usize) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT rtk_cmd, AVG(savings_pct) as avg_sav FROM commands - WHERE input_tokens > 0 - GROUP BY rtk_cmd - HAVING avg_sav < 30.0 AND avg_sav > 0.0 - ORDER BY COUNT(*) DESC LIMIT ?1", - )?; - let rows = stmt.query_map(params![limit as i64], |row| { - let cmd: String = row.get(0)?; - let sav: f64 = row.get(1)?; - let short = cmd.split_whitespace().take(3).collect::>().join(" "); - Ok((short, sav)) - })?; - Ok(rows.filter_map(|r| r.ok()).collect()) - } - - /// Average savings percentage per command (unweighted — each command name counts once). - pub fn avg_savings_per_command(&self) -> Result { - let avg: f64 = self.conn.query_row( - "SELECT COALESCE(AVG(avg_sav), 0.0) FROM ( - SELECT rtk_cmd, AVG(savings_pct) as avg_sav - FROM commands WHERE input_tokens > 0 - GROUP BY rtk_cmd - )", - [], - |row| row.get(0), - )?; - Ok(avg) - } - - /// Count invocations of a specific meta-command (by rtk_cmd suffix). - pub fn count_meta_command(&self, name: &str) -> Result { - let pattern = format!("rtk {}", name); - let count: i64 = self.conn.query_row( - "SELECT COUNT(*) FROM commands WHERE rtk_cmd LIKE ?1 || '%'", - params![pattern], - |row| row.get(0), - )?; - Ok(count) - } - - /// Days since first recorded command (installation age). - pub fn first_seen_days(&self) -> Result { - let oldest: Option = - match self - .conn - .query_row("SELECT MIN(timestamp) FROM commands", [], |row| row.get(0)) - { - Ok(v) => v, - Err(rusqlite::Error::QueryReturnedNoRows) => None, - Err(e) => return Err(anyhow::anyhow!("Failed to query first seen timestamp: {e}")), - }; - match oldest { - Some(ts) => { - let first = chrono::NaiveDateTime::parse_from_str(&ts, "%Y-%m-%dT%H:%M:%S") - .or_else(|_| chrono::NaiveDateTime::parse_from_str(&ts, "%Y-%m-%d %H:%M:%S")) - .map(|dt| dt.and_utc()) - .unwrap_or_else(|_| chrono::Utc::now()); - let days = (chrono::Utc::now() - first).num_days(); - Ok(days.max(0)) - } - None => Ok(0), - } - } - - /// Number of distinct active days in the last 30 days. - pub fn active_days_30d(&self) -> Result { - let since = (chrono::Utc::now() - chrono::Duration::days(30)) - .format("%Y-%m-%dT%H:%M:%S") - .to_string(); - let count: i64 = self.conn.query_row( - "SELECT COUNT(DISTINCT DATE(timestamp)) FROM commands WHERE timestamp >= ?1", - params![since], - |row| row.get(0), - )?; - Ok(count) - } - - /// Total number of recorded commands. - pub fn commands_total(&self) -> Result { - let count: i64 = self - .conn - .query_row("SELECT COUNT(*) FROM commands", [], |row| row.get(0))?; - Ok(count) - } - - /// Ecosystem distribution as percentages (top categories by command prefix). - pub fn ecosystem_mix(&self) -> Result> { - let total: f64 = self.conn.query_row( - "SELECT COUNT(*) FROM commands WHERE input_tokens > 0 AND timestamp >= datetime('now', '-90 days')", - [], - |row| row.get(0), - )?; - if total == 0.0 { - return Ok(vec![]); - } - let mut stmt = self.conn.prepare( - "SELECT rtk_cmd, COUNT(*) as cnt FROM commands - WHERE input_tokens > 0 AND timestamp >= datetime('now', '-90 days') - GROUP BY rtk_cmd ORDER BY cnt DESC", - )?; - let mut categories: std::collections::HashMap = - std::collections::HashMap::new(); - let rows = stmt.query_map([], |row| { - let cmd: String = row.get(0)?; - let cnt: f64 = row.get(1)?; - Ok((cmd, cnt)) - })?; - for row in rows.flatten() { - let cat = categorize_command(&row.0); - *categories.entry(cat).or_default() += row.1; - } - let mut result: Vec<(String, f64)> = categories - .into_iter() - .map(|(cat, cnt)| (cat, (cnt / total * 100.0).round())) - .collect(); - result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - result.truncate(8); - Ok(result) - } - - /// Tokens saved in the last 30 days. - pub fn tokens_saved_30d(&self) -> Result { - let since = (chrono::Utc::now() - chrono::Duration::days(30)) - .format("%Y-%m-%dT%H:%M:%S") - .to_string(); - let saved: i64 = self.conn.query_row( - "SELECT COALESCE(SUM(saved_tokens), 0) FROM commands WHERE timestamp >= ?1", - params![since], - |row| row.get(0), - )?; - Ok(saved) - } - - /// Number of distinct project paths. - pub fn projects_count(&self) -> Result { - let count: i64 = self.conn.query_row( - "SELECT COUNT(DISTINCT project_path) FROM commands WHERE project_path != ''", - [], - |row| row.get(0), - )?; - Ok(count) - } -} - -/// Map an rtk_cmd to an ecosystem category for telemetry. -fn categorize_command(rtk_cmd: &str) -> String { - let parts: Vec<&str> = rtk_cmd.split_whitespace().collect(); - let tool = parts.get(1).copied().unwrap_or("other"); - match tool { - "git" | "gh" | "gt" => "git", - "cargo" => "cargo", - "npm" | "npx" | "pnpm" | "vitest" | "tsc" | "lint" | "prettier" | "next" | "playwright" - | "prisma" => "js", - "pytest" | "ruff" | "mypy" | "pip" => "python", - "go" | "golangci-lint" => "go", - "docker" | "kubectl" => "cloud", - "rspec" | "rubocop" | "rake" => "ruby", - "dotnet" => "dotnet", - "ls" | "tree" | "grep" | "find" | "wc" | "read" | "env" | "json" | "log" | "smart" - | "diff" | "deps" | "summary" | "format" => "system", - _ => "other", - } - .to_string() } fn get_db_path() -> Result { diff --git a/src/core/utils.rs b/src/core/utils.rs index feae1fb6d..cbfe6c893 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -95,46 +95,6 @@ pub fn format_tokens(n: usize) -> String { /// /// # Examples /// ``` -/// use rtk::utils::format_usd; -/// assert_eq!(format_usd(1234.567), "$1234.57"); -/// assert_eq!(format_usd(12.345), "$12.35"); -/// assert_eq!(format_usd(0.123), "$0.12"); -/// assert_eq!(format_usd(0.0096), "$0.0096"); -/// ``` -pub fn format_usd(amount: f64) -> String { - if !amount.is_finite() { - return "$0.00".to_string(); - } - if amount >= 0.01 { - format!("${:.2}", amount) - } else { - format!("${:.4}", amount) - } -} - -/// Format cost-per-token as $/MTok (e.g., "$3.86/MTok") -/// -/// # Arguments -/// * `cpt` - Cost per token (not per million tokens) -/// -/// # Returns -/// Formatted string like "$3.86/MTok" -/// -/// # Examples -/// ``` -/// use rtk::utils::format_cpt; -/// assert_eq!(format_cpt(0.000003), "$3.00/MTok"); -/// assert_eq!(format_cpt(0.0000038), "$3.80/MTok"); -/// assert_eq!(format_cpt(0.00000386), "$3.86/MTok"); -/// ``` -pub fn format_cpt(cpt: f64) -> String { - if !cpt.is_finite() || cpt <= 0.0 { - return "$0.00/MTok".to_string(); - } - let cpt_per_million = cpt * 1_000_000.0; - format!("${:.2}/MTok", cpt_per_million) -} - /// Join items into a newline-separated string, appending an overflow hint when total > max. /// /// # Examples @@ -408,6 +368,27 @@ pub fn human_bytes(bytes: u64) -> String { mod tests { use super::*; + /// Format a currency amount as USD (internal test function). + fn format_usd(amount: f64) -> String { + if !amount.is_finite() { + return "$0.00".to_string(); + } + if amount >= 0.01 { + format!("${:.2}", amount) + } else { + format!("${:.4}", amount) + } + } + + /// Format cost-per-token as $/MTok (internal test function). + fn format_cpt(cpt: f64) -> String { + if !cpt.is_finite() || cpt <= 0.0 { + return "$0.00/MTok".to_string(); + } + let cpt_per_million = cpt * 1_000_000.0; + format!("${:.2}/MTok", cpt_per_million) + } + #[test] fn test_truncate_short_string() { assert_eq!(truncate("hello", 10), "hello"); diff --git a/src/discover/README.md b/src/discover/README.md index 4897fe870..6d78c7fd5 100644 --- a/src/discover/README.md +++ b/src/discover/README.md @@ -1,73 +1,5 @@ -# Discover — History Analysis & Command Rewrite +# discover/ -> Full rewrite pipeline diagram: [docs/contributing/TECHNICAL.md](../../docs/contributing/TECHNICAL.md#32-hook-interception-command-rewriting) +Internal rewrite registry and parsing helpers used by hook and rewrite flows. -## What This Module Does - -This module has two jobs: - -1. **Rewrite commands** — Every LLM agent hook calls `rtk rewrite "git status"`. This module decides whether to rewrite it (`rtk git status`) or pass it through unchanged. This is the hot path — every command the LLM runs goes through here. - -2. **Analyze history** — `rtk discover` scans past LLM sessions to find commands that *could have been* rewritten but weren't. Same classification logic, different consumer. - -## How Command Rewriting Works - -When a hook sends `cargo fmt --all && cargo test 2>&1 | tail -20`: - -**Tokenization** — The lexer (`lexer.rs`) turns the raw string into typed tokens. It's a single-pass state machine that understands shell quoting, escapes, redirects, and operators. This is critical because naive string splitting breaks on quoted content like `git commit -m "fix && update"`. - -``` -"cargo test 2>&1 && git status" -→ [Arg("cargo"), Arg("test"), Redirect("2>&1"), Operator("&&"), Arg("git"), Arg("status")] -``` - -**Compound splitting** — The rewrite engine walks the tokens, splitting on `Operator` (`&&`, `||`, `;`) and `Pipe` (`|`). Each segment is rewritten independently. For pipes, only the left side is rewritten (the pipe consumer like `grep` or `head` runs raw). `find`/`fd` before a pipe is never rewritten because rtk's grouped output format breaks pipe consumers like `xargs`. - -**Per-segment rewriting** — Each segment goes through: - -1. Strip trailing redirects (`2>&1`, `>/dev/null`) — matched via lexer tokens, set aside, re-appended after rewriting -2. Short-circuit special cases — `head -20 file` → `rtk read file --max-lines 20`, `tail -n 5 file` → `rtk read file --tail-lines 5`. These can't go through generic prefix replacement because it would produce `rtk read -20 file` (wrong flag position) -3. Classify the command — strip env prefixes (`sudo`, `FOO="bar baz"`), normalize paths (`/usr/bin/grep` → `grep`), strip git global opts (`git -C /tmp` → `git`), then match against 60+ regex patterns from `rules.rs` -4. Apply the rewrite — find the matching rule, replace the command prefix with `rtk `, re-prepend the env prefix, re-append the redirect suffix - -**Guards along the way:** -- `RTK_DISABLED=1` in the env prefix → skip rewrite -- `gh` with `--json`/`--jq`/`--template` → skip (structured output, rtk would corrupt it) -- `cat` with flags other than `-n` → skip (different semantics than `rtk read`) -- `cat`/`head`/`tail` with `>` or `>>` → skip (write operation, not a read) -- Command in `hooks.exclude_commands` config → skip - -**Result**: `rtk cargo fmt --all && rtk cargo test 2>&1 | tail -20`. Bash handles the `&&` and `|` at execution time — each `rtk` invocation is a separate process. - -## How History Analysis Works - -`rtk discover` reads Claude Code JSONL session files. Each file contains `tool_use`/`tool_result` pairs for every command the LLM ran. The module: - -1. Extracts commands from the JSONL (via `SessionProvider` trait — currently only Claude Code) -2. Splits compound commands using the same lexer-based tokenization -3. Classifies each command against the same rules used for live rewriting -4. Aggregates results: which commands could have been rewritten, estimated token savings, adoption rate - -The classification logic is shared between discover and rewrite — same patterns, same rules, different consumers. - -## Env Prefix Handling - -The `ENV_PREFIX` regex strips env variable assignments, `sudo`, and `env` from the front of commands. It handles: -- Unquoted: `FOO=bar` -- Double-quoted with spaces: `FOO="bar baz"` -- Single-quoted: `FOO='bar baz'` -- Escaped quotes: `FOO="he said \"hello\""` -- Chained: `A="x y" B=1 sudo git status` - -The prefix is stripped twice: once in `classify_command()` to match the underlying command against rules, and again in `rewrite_segment()` to extract it for re-prepending to the rewritten command. - -## Adding a New Rewrite Rule - -Add an entry to `rules.rs`. Each rule has: -- `pattern` — regex that matches the command (used by `RegexSet` for fast matching) -- `rtk_cmd` — the RTK command it maps to (e.g., `"rtk cargo"`) -- `rewrite_prefixes` — command prefixes to replace (e.g., `&["cargo"]`) -- `category`, `savings_pct` — metadata for discover reports -- `subcmd_savings`, `subcmd_status` — per-subcommand overrides - -No other files need to change. The registry compiles the patterns at first use via `lazy_static`. +This fork does not expose a user-facing `rtk discover` analytics command. diff --git a/src/discover/mod.rs b/src/discover/mod.rs index e5b4a87b8..af534a4f4 100644 --- a/src/discover/mod.rs +++ b/src/discover/mod.rs @@ -1,294 +1,6 @@ -//! Scans AI coding sessions to find commands that could benefit from RTK filtering. +//! Rewrite parsing/registry internals used by hook flows. pub mod lexer; -pub mod provider; pub mod registry; -mod report; pub mod rules; - -use anyhow::Result; -use std::collections::HashMap; - -use provider::{ClaudeProvider, SessionProvider}; -use registry::{ - category_avg_tokens, classify_command, has_rtk_disabled_prefix, split_command_chain, - strip_disabled_prefix, Classification, -}; -use report::{DiscoverReport, SupportedEntry, UnsupportedEntry}; - -/// Aggregation bucket for supported commands. -struct SupportedBucket { - rtk_equivalent: &'static str, - category: &'static str, - count: usize, - /// Total estimated tokens *saved* (post-filter). Used for the "Est. Savings" column. - total_output_tokens: usize, - /// Total estimated tokens *before* filtering (raw output). Accumulated alongside - /// `total_output_tokens` so the bucket's effective savings rate can be derived as - /// `total_output_tokens / total_raw_output_tokens` — a weighted average across - /// all sub-commands, regardless of which sub-command was seen first. - total_raw_output_tokens: usize, - // For display: the most common raw command - command_counts: HashMap, -} - -/// Aggregation bucket for unsupported commands. -struct UnsupportedBucket { - count: usize, - example: String, -} - -pub fn run( - project: Option<&str>, - all: bool, - since_days: u64, - limit: usize, - format: &str, - verbose: u8, -) -> Result<()> { - let provider = ClaudeProvider; - - // Determine project filter - let project_filter = if all { - None - } else if let Some(p) = project { - Some(p.to_string()) - } else { - // Default: current working directory - let cwd = std::env::current_dir()?; - let cwd_str = cwd.to_string_lossy().to_string(); - let encoded = ClaudeProvider::encode_project_path(&cwd_str); - Some(encoded) - }; - - let sessions = provider.discover_sessions(project_filter.as_deref(), Some(since_days))?; - - if verbose > 0 { - eprintln!("Scanning {} session files...", sessions.len()); - for s in &sessions { - eprintln!(" {}", s.display()); - } - } - - let mut total_commands: usize = 0; - let mut already_rtk: usize = 0; - let mut parse_errors: usize = 0; - let mut rtk_disabled_count: usize = 0; - let mut rtk_disabled_cmds: HashMap = HashMap::new(); - let mut supported_map: HashMap<&'static str, SupportedBucket> = HashMap::new(); - let mut unsupported_map: HashMap = HashMap::new(); - - for session_path in &sessions { - let extracted = match provider.extract_commands(session_path) { - Ok(cmds) => cmds, - Err(e) => { - if verbose > 0 { - eprintln!("Warning: skipping {}: {}", session_path.display(), e); - } - parse_errors += 1; - continue; - } - }; - - for ext_cmd in &extracted { - let parts = split_command_chain(&ext_cmd.command); - for part in parts { - total_commands += 1; - - // Detect RTK_DISABLED= bypass before classification - if has_rtk_disabled_prefix(part) { - let actual_cmd = strip_disabled_prefix(part); - // Only count if the underlying command is one RTK supports - match classify_command(actual_cmd) { - Classification::Supported { .. } => { - rtk_disabled_count += 1; - let display = truncate_command(actual_cmd); - *rtk_disabled_cmds.entry(display).or_insert(0) += 1; - } - _ => { - // RTK_DISABLED on unsupported/ignored command — not interesting - } - } - continue; - } - - match classify_command(part) { - Classification::Supported { - rtk_equivalent, - category, - estimated_savings_pct, - status, - } => { - let bucket = supported_map.entry(rtk_equivalent).or_insert_with(|| { - SupportedBucket { - rtk_equivalent, - category, - count: 0, - total_output_tokens: 0, - total_raw_output_tokens: 0, - command_counts: HashMap::new(), - } - }); - - bucket.count += 1; - - // Estimate tokens for this command - let output_tokens = if let Some(len) = ext_cmd.output_len { - // Real: from tool_result content length - len / 4 - } else { - // Fallback: category average - let subcmd = extract_subcmd(part); - category_avg_tokens(category, subcmd) - }; - - let savings = - (output_tokens as f64 * estimated_savings_pct / 100.0) as usize; - bucket.total_output_tokens += savings; - // Accumulate pre-savings tokens so we can compute a weighted effective - // savings rate across all sub-commands in this bucket later. - bucket.total_raw_output_tokens += output_tokens; - - // Track the display name with status - let display_name = truncate_command(part); - let entry = bucket - .command_counts - .entry(format!("{}:{:?}", display_name, status)) - .or_insert(0); - *entry += 1; - } - Classification::Unsupported { base_command } => { - let bucket = unsupported_map.entry(base_command).or_insert_with(|| { - UnsupportedBucket { - count: 0, - example: part.to_string(), - } - }); - bucket.count += 1; - } - Classification::Ignored => { - // Check if it starts with "rtk " - if part.trim().starts_with("rtk ") { - already_rtk += 1; - } - // Otherwise just skip - } - } - } - } - } - - // Build report - let mut supported: Vec = supported_map - .into_values() - .map(|bucket| { - // Pick the most common command as the display name - let (command_with_status, status) = bucket - .command_counts - .into_iter() - .max_by_key(|(_, c)| *c) - .map(|(name, _)| { - // Extract status from "command:Status" format - if let Some(colon_pos) = name.rfind(':') { - let cmd = name[..colon_pos].to_string(); - let status_str = &name[colon_pos + 1..]; - let status = match status_str { - "Passthrough" => report::RtkStatus::Passthrough, - "NotSupported" => report::RtkStatus::NotSupported, - _ => report::RtkStatus::Existing, - }; - (cmd, status) - } else { - (name, report::RtkStatus::Existing) - } - }) - .unwrap_or_else(|| (String::new(), report::RtkStatus::Existing)); - - // Derive the effective savings rate from accumulated totals rather than - // using the first-seen sub-command's rate. This gives a weighted average - // across all sub-commands that fell in this bucket. - let effective_savings_pct = if bucket.total_raw_output_tokens > 0 { - bucket.total_output_tokens as f64 * 100.0 / bucket.total_raw_output_tokens as f64 - } else { - 0.0 - }; - - SupportedEntry { - command: command_with_status, - count: bucket.count, - rtk_equivalent: bucket.rtk_equivalent, - category: bucket.category, - estimated_savings_tokens: bucket.total_output_tokens, - estimated_savings_pct: effective_savings_pct, - rtk_status: status, - } - }) - .collect(); - - // Sort by estimated savings descending - supported.sort_by(|a, b| b.estimated_savings_tokens.cmp(&a.estimated_savings_tokens)); - - let mut unsupported: Vec = unsupported_map - .into_iter() - .map(|(base, bucket)| UnsupportedEntry { - base_command: base, - count: bucket.count, - example: bucket.example, - }) - .collect(); - - // Sort by count descending - unsupported.sort_by(|a, b| b.count.cmp(&a.count)); - - // Build RTK_DISABLED examples sorted by frequency (top 5) - let rtk_disabled_examples: Vec = { - let mut sorted: Vec<_> = rtk_disabled_cmds.into_iter().collect(); - sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); - sorted - .into_iter() - .take(5) - .map(|(cmd, count)| format!("{} ({}x)", cmd, count)) - .collect() - }; - - let report = DiscoverReport { - sessions_scanned: sessions.len(), - total_commands, - already_rtk, - since_days, - supported, - unsupported, - parse_errors, - rtk_disabled_count, - rtk_disabled_examples, - }; - - match format { - "json" => println!("{}", report::format_json(&report)), - _ => print!("{}", report::format_text(&report, limit, verbose > 0)), - } - - Ok(()) -} - -/// Extract the subcommand from a command string (second word). -fn extract_subcmd(cmd: &str) -> &str { - let parts: Vec<&str> = cmd.trim().splitn(3, char::is_whitespace).collect(); - if parts.len() >= 2 { - parts[1] - } else { - "" - } -} - -/// Truncate a command for display (keep first meaningful portion). -fn truncate_command(cmd: &str) -> String { - let trimmed = cmd.trim(); - // Keep first two words for display - let parts: Vec<&str> = trimmed.splitn(3, char::is_whitespace).collect(); - match parts.len() { - 0 => String::new(), - 1 => parts[0].to_string(), - _ => format!("{} {}", parts[0], parts[1]), - } -} +pub mod status; diff --git a/src/discover/provider.rs b/src/discover/provider.rs deleted file mode 100644 index 08b4ddc8f..000000000 --- a/src/discover/provider.rs +++ /dev/null @@ -1,461 +0,0 @@ -//! Reads Claude Code session logs from disk and streams their command history. - -use crate::hooks::constants::CLAUDE_DIR; -use anyhow::{Context, Result}; -use std::collections::HashMap; -use std::fs; -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; -use walkdir::WalkDir; - -/// A command extracted from a session file. -#[derive(Debug)] -pub struct ExtractedCommand { - pub command: String, - pub output_len: Option, - #[allow(dead_code)] - pub session_id: String, - /// Actual output content (first ~1000 chars for error detection) - pub output_content: Option, - /// Whether the tool_result indicated an error - pub is_error: bool, - /// Chronological sequence index within the session - #[allow(dead_code)] - pub sequence_index: usize, -} - -/// Trait for session providers (Claude Code, OpenCode, etc.). -/// -/// Note: Cursor Agent transcripts use a text-only format without structured -/// tool_use/tool_result blocks, so command extraction is not possible. -/// Use `rtk gain` to track savings for Cursor sessions instead. -pub trait SessionProvider { - fn discover_sessions( - &self, - project_filter: Option<&str>, - since_days: Option, - ) -> Result>; - fn extract_commands(&self, path: &Path) -> Result>; -} - -pub struct ClaudeProvider; - -impl ClaudeProvider { - /// Get the base directory for Claude Code projects. - fn projects_dir() -> Result { - let home = dirs::home_dir().context("could not determine home directory")?; - let dir = home.join(CLAUDE_DIR).join("projects"); - if !dir.exists() { - anyhow::bail!( - "Claude Code projects directory not found: {}\nMake sure Claude Code has been used at least once.", - dir.display() - ); - } - Ok(dir) - } - - /// Encode a filesystem path to Claude Code's directory name format. - /// - /// Claude Code replaces `/`, `.`, `_`, `\`, and any non-ASCII character - /// with `-` when computing the project directory slug under `~/.claude/projects/`. - /// - /// `/Users/foo/bar` → `-Users-foo-bar` - /// `/Users/first.last/bar` → `-Users-first-last-bar` - /// `/home/chris/2_project` → `-home-chris-2-project` - /// `C:\Users\foo\bar` → `C:-Users-foo-bar` - pub fn encode_project_path(path: &str) -> String { - const SANITIZED_CHARS: &[char] = &['/', '.', '_', '\\']; - - path.chars() - .map(|c| { - if !c.is_ascii() || SANITIZED_CHARS.contains(&c) { - '-' - } else { - c - } - }) - .collect() - } -} - -impl SessionProvider for ClaudeProvider { - fn discover_sessions( - &self, - project_filter: Option<&str>, - since_days: Option, - ) -> Result> { - let projects_dir = Self::projects_dir()?; - let cutoff = since_days.map(|days| { - SystemTime::now() - .checked_sub(Duration::from_secs(days * 86400)) - .unwrap_or(SystemTime::UNIX_EPOCH) - }); - - let mut sessions = Vec::new(); - - // List project directories - let entries = fs::read_dir(&projects_dir) - .with_context(|| format!("failed to read {}", projects_dir.display()))?; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - // Apply project filter: substring match on directory name - if let Some(filter) = project_filter { - let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); - if !dir_name.contains(filter) { - continue; - } - } - - // Walk the project directory recursively (catches subagents/) - for walk_entry in WalkDir::new(&path) - .follow_links(false) - .into_iter() - .filter_map(|e| e.ok()) - { - let file_path = walk_entry.path(); - if file_path.extension().and_then(|e| e.to_str()) != Some("jsonl") { - continue; - } - - // Apply mtime filter - if let Some(cutoff_time) = cutoff { - if let Ok(meta) = fs::metadata(file_path) { - if let Ok(mtime) = meta.modified() { - if mtime < cutoff_time { - continue; - } - } - } - } - - sessions.push(file_path.to_path_buf()); - } - } - - Ok(sessions) - } - - fn extract_commands(&self, path: &Path) -> Result> { - let file = - fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?; - let reader = BufReader::new(file); - - let session_id = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown") - .to_string(); - - // First pass: collect all tool_use Bash commands with their IDs and sequence - // Second pass (same loop): collect tool_result output lengths, content, and error status - let mut pending_tool_uses: Vec<(String, String, usize)> = Vec::new(); // (tool_use_id, command, sequence) - let mut tool_results: HashMap = HashMap::new(); // (len, content, is_error) - let mut commands = Vec::new(); - let mut sequence_counter = 0; - - for line in reader.lines() { - let line = match line { - Ok(l) => l, - Err(_) => continue, - }; - - // Pre-filter: skip lines that can't contain Bash tool_use or tool_result - if !line.contains("\"Bash\"") && !line.contains("\"tool_result\"") { - continue; - } - - let entry: serde_json::Value = match serde_json::from_str(&line) { - Ok(v) => v, - Err(_) => continue, - }; - - let entry_type = entry.get("type").and_then(|t| t.as_str()).unwrap_or(""); - - match entry_type { - "assistant" => { - // Look for tool_use Bash blocks in message.content - if let Some(content) = - entry.pointer("/message/content").and_then(|c| c.as_array()) - { - for block in content { - if block.get("type").and_then(|t| t.as_str()) == Some("tool_use") - && block.get("name").and_then(|n| n.as_str()) == Some("Bash") - { - if let (Some(id), Some(cmd)) = ( - block.get("id").and_then(|i| i.as_str()), - block.pointer("/input/command").and_then(|c| c.as_str()), - ) { - pending_tool_uses.push(( - id.to_string(), - cmd.to_string(), - sequence_counter, - )); - sequence_counter += 1; - } - } - } - } - } - "user" => { - // Look for tool_result blocks - if let Some(content) = - entry.pointer("/message/content").and_then(|c| c.as_array()) - { - for block in content { - if block.get("type").and_then(|t| t.as_str()) == Some("tool_result") { - if let Some(id) = block.get("tool_use_id").and_then(|i| i.as_str()) - { - // Get content, length, and error status - let content = - block.get("content").and_then(|c| c.as_str()).unwrap_or(""); - - let output_len = content.len(); - let is_error = block - .get("is_error") - .and_then(|e| e.as_bool()) - .unwrap_or(false); - - // Store first ~1000 chars of content for error detection - let content_preview: String = - content.chars().take(1000).collect(); - - tool_results.insert( - id.to_string(), - (output_len, content_preview, is_error), - ); - } - } - } - } - } - _ => {} - } - } - - // Match tool_uses with their results - for (tool_id, command, sequence_index) in pending_tool_uses { - let (output_len, output_content, is_error) = tool_results - .get(&tool_id) - .map(|(len, content, err)| (Some(*len), Some(content.clone()), *err)) - .unwrap_or((None, None, false)); - - commands.push(ExtractedCommand { - command, - output_len, - session_id: session_id.clone(), - output_content, - is_error, - sequence_index, - }); - } - - Ok(commands) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - - fn make_jsonl(lines: &[&str]) -> tempfile::NamedTempFile { - let mut f = tempfile::NamedTempFile::new().unwrap(); - for line in lines { - writeln!(f, "{}", line).unwrap(); - } - f.flush().unwrap(); - f - } - - #[test] - fn test_extract_assistant_bash() { - let jsonl = make_jsonl(&[ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_abc","name":"Bash","input":{"command":"git status"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_abc","content":"On branch master\nnothing to commit"}]}}"#, - ]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 1); - assert_eq!(cmds[0].command, "git status"); - assert!(cmds[0].output_len.is_some()); - assert_eq!( - cmds[0].output_len.unwrap(), - "On branch master\nnothing to commit".len() - ); - } - - #[test] - fn test_extract_non_bash_ignored() { - let jsonl = make_jsonl(&[ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_abc","name":"Read","input":{"file_path":"/tmp/foo"}}]}}"#, - ]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 0); - } - - #[test] - fn test_extract_non_message_ignored() { - let jsonl = - make_jsonl(&[r#"{"type":"file-history-snapshot","messageId":"abc","snapshot":{}}"#]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 0); - } - - #[test] - fn test_extract_multiple_tools() { - let jsonl = make_jsonl(&[ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"git status"}},{"type":"tool_use","id":"toolu_2","name":"Bash","input":{"command":"git diff"}}]}}"#, - ]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 2); - assert_eq!(cmds[0].command, "git status"); - assert_eq!(cmds[1].command, "git diff"); - } - - #[test] - fn test_extract_malformed_line() { - let jsonl = make_jsonl(&[ - "this is not json at all", - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_ok","name":"Bash","input":{"command":"ls"}}]}}"#, - ]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 1); - assert_eq!(cmds[0].command, "ls"); - } - - #[test] - fn test_encode_project_path() { - assert_eq!( - ClaudeProvider::encode_project_path("/Users/foo/bar"), - "-Users-foo-bar" - ); - } - - #[test] - fn test_encode_project_path_trailing_slash() { - assert_eq!( - ClaudeProvider::encode_project_path("/Users/foo/bar/"), - "-Users-foo-bar-" - ); - } - - #[test] - fn test_encode_project_path_dot_in_username() { - // Claude Code replaces both '/' and '.' with '-'. - // A cwd like /Users/first.last must produce the same slug as - // Claude's projects directory (-Users-first-last), otherwise - // `rtk discover` finds zero sessions for that project. - assert_eq!( - ClaudeProvider::encode_project_path("/Users/first.last/my-project"), - "-Users-first-last-my-project" - ); - } - - #[test] - fn test_encode_project_path_multiple_dots() { - assert_eq!( - ClaudeProvider::encode_project_path("/Users/a.b.c/proj"), - "-Users-a-b-c-proj" - ); - } - - #[test] - fn test_encode_project_path_underscore() { - // Claude Code also replaces '_' with '-' (https://github.com/anthropics/claude-code/issues/24067) - assert_eq!( - ClaudeProvider::encode_project_path("/home/chris/2_project-files/proj"), - "-home-chris-2-project-files-proj" - ); - } - - #[test] - fn test_encode_project_path_non_ascii() { - // Non-ASCII characters are each replaced with '-' (https://github.com/anthropics/claude-code/issues/40946) - // '/home/user/' + '外' + '主' + '/app' -> '-home-user' + '-' + '-' + '-' + '-' + 'app' - assert_eq!( - ClaudeProvider::encode_project_path("/home/user/\u{5916}\u{4e3b}/app"), - "-home-user----app" - ); - } - - #[test] - fn test_encode_project_path_windows() { - // Windows backslashes are also replaced with '-' - assert_eq!( - ClaudeProvider::encode_project_path(r"C:\Users\foo\bar"), - "C:-Users-foo-bar" - ); - } - - #[test] - fn test_match_project_filter() { - let encoded = ClaudeProvider::encode_project_path("/Users/foo/Sites/rtk"); - assert!(encoded.contains("rtk")); - assert!(encoded.contains("Sites")); - } - - #[test] - fn test_extract_output_content() { - let jsonl = make_jsonl(&[ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_abc","name":"Bash","input":{"command":"git commit --ammend"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_abc","content":"error: unexpected argument '--ammend'","is_error":true}]}}"#, - ]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 1); - assert_eq!(cmds[0].command, "git commit --ammend"); - assert!(cmds[0].is_error); - assert!(cmds[0].output_content.is_some()); - assert_eq!( - cmds[0].output_content.as_ref().unwrap(), - "error: unexpected argument '--ammend'" - ); - } - - #[test] - fn test_extract_is_error_flag() { - let jsonl = make_jsonl(&[ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"ls"}},{"type":"tool_use","id":"toolu_2","name":"Bash","input":{"command":"invalid_cmd"}}]}}"#, - r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"file1.txt","is_error":false},{"type":"tool_result","tool_use_id":"toolu_2","content":"command not found","is_error":true}]}}"#, - ]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 2); - assert!(!cmds[0].is_error); - assert!(cmds[1].is_error); - } - - #[test] - fn test_extract_sequence_ordering() { - let jsonl = make_jsonl(&[ - r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"first"}},{"type":"tool_use","id":"toolu_2","name":"Bash","input":{"command":"second"}},{"type":"tool_use","id":"toolu_3","name":"Bash","input":{"command":"third"}}]}}"#, - ]); - - let provider = ClaudeProvider; - let cmds = provider.extract_commands(jsonl.path()).unwrap(); - assert_eq!(cmds.len(), 3); - assert_eq!(cmds[0].sequence_index, 0); - assert_eq!(cmds[1].sequence_index, 1); - assert_eq!(cmds[2].sequence_index, 2); - assert_eq!(cmds[0].command, "first"); - assert_eq!(cmds[1].command, "second"); - assert_eq!(cmds[2].command, "third"); - } -} diff --git a/src/discover/registry.rs b/src/discover/registry.rs index 3921089ca..9ae360fc2 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -3,7 +3,7 @@ use lazy_static::lazy_static; use regex::{Regex, RegexSet}; -use super::lexer::{split_on_operators, tokenize, TokenKind}; +use super::lexer::{tokenize, TokenKind}; use super::rules::{IGNORED_EXACT, IGNORED_PREFIXES, RULES}; /// Result of classifying a command. @@ -13,7 +13,7 @@ pub enum Classification { rtk_equivalent: &'static str, category: &'static str, estimated_savings_pct: f64, - status: super::report::RtkStatus, + status: super::status::RtkStatus, }, Unsupported { base_command: String, @@ -21,29 +21,6 @@ pub enum Classification { Ignored, } -/// Average token counts per category for estimation when no output_len available. -pub fn category_avg_tokens(category: &str, subcmd: &str) -> usize { - match category { - "Git" => match subcmd { - "log" | "diff" | "show" => 200, - _ => 40, - }, - "Cargo" => match subcmd { - "test" => 500, - _ => 150, - }, - "Tests" => 800, - "Files" => 100, - "Build" => 300, - "Infra" => 120, - "Network" => 150, - "GitHub" => 200, - "GitLab" => 200, - "PackageManager" => 150, - _ => 150, - } -} - lazy_static! { static ref REGEX_SET: RegexSet = RegexSet::new(RULES.iter().map(|r| r.pattern)).expect("invalid regex patterns"); @@ -160,7 +137,7 @@ pub fn classify_command(cmd: &str) -> Classification { .iter() .find(|(s, _)| *s == subcmd) .map(|(_, st)| *st) - .unwrap_or(super::report::RtkStatus::Existing); + .unwrap_or(super::status::RtkStatus::Existing); // Check if this subcommand has custom savings let savings = rule @@ -172,10 +149,10 @@ pub fn classify_command(cmd: &str) -> Classification { (savings, status) } else { - (rule.savings_pct, super::report::RtkStatus::Existing) + (rule.savings_pct, super::status::RtkStatus::Existing) } } else { - (rule.savings_pct, super::report::RtkStatus::Existing) + (rule.savings_pct, super::status::RtkStatus::Existing) }; Classification::Supported { @@ -233,20 +210,6 @@ pub fn has_heredoc(cmd: &str) -> bool { .any(|t| t.kind == TokenKind::Redirect && t.value.starts_with("<<")) } -pub fn split_command_chain(cmd: &str) -> Vec<&str> { - let trimmed = cmd.trim(); - if trimmed.is_empty() { - return vec![]; - } - - // Lexer-based for `<<`; string-based for `$((` (lexer splits it across tokens). - if has_heredoc(trimmed) || trimmed.contains("$((") { - return vec![trimmed]; - } - - split_on_operators(trimmed, true) -} - /// Strip git global options before the subcommand (#163). /// `git -C /tmp status` → `git status`, preserving the rest. /// Returns the original string unchanged if not a git command. @@ -392,15 +355,6 @@ pub fn has_rtk_disabled_prefix(cmd: &str) -> bool { } /// Strip RTK_DISABLED=X and other env prefixes, return the actual command. -pub fn strip_disabled_prefix(cmd: &str) -> &str { - let trimmed = cmd.trim(); - let stripped = ENV_PREFIX.replace(trimmed, ""); - // stripped is a Cow that borrows from trimmed when no replacement happens. - // We need to return a &str into the original, so compute the offset. - let prefix_len = trimmed.len() - stripped.len(); - trimmed[prefix_len..].trim_start() -} - fn strip_trailing_redirects(cmd: &str) -> (&str, &str) { let tokens = tokenize(cmd); if tokens.is_empty() { @@ -780,8 +734,32 @@ fn strip_word_prefix<'a>(cmd: &'a str, prefix: &str) -> Option<&'a str> { #[cfg(test)] mod tests { - use super::super::report::RtkStatus; + use super::super::status::RtkStatus; use super::*; + use crate::discover::lexer::split_on_operators; + + fn split_command_chain(cmd: &str) -> Vec<&str> { + let trimmed = cmd.trim(); + if trimmed.is_empty() { + return vec![]; + } + + // Lexer-based for `<<`; string-based for `$((` (lexer splits it across tokens). + if has_heredoc(trimmed) || trimmed.contains("$((") { + return vec![trimmed]; + } + + split_on_operators(trimmed, true) + } + + fn strip_disabled_prefix(cmd: &str) -> &str { + let trimmed = cmd.trim(); + let stripped = ENV_PREFIX.replace(trimmed, ""); + // stripped is a Cow that borrows from trimmed when no replacement happens. + // We need to return a &str into the original, so compute the offset. + let prefix_len = trimmed.len() - stripped.len(); + trimmed[prefix_len..].trim_start() + } #[test] fn test_classify_git_status() { diff --git a/src/discover/report.rs b/src/discover/report.rs deleted file mode 100644 index 2af129a5c..000000000 --- a/src/discover/report.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Data types for reporting which commands RTK can and cannot optimize. - -use crate::hooks::constants::{CURSOR_DIR, HOOKS_SUBDIR, REWRITE_HOOK_FILE}; -use serde::Serialize; - -/// RTK support status for a command. -#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)] -pub enum RtkStatus { - /// Dedicated handler with filtering (e.g., git status → git.rs:run_status()) - Existing, - /// Works via external_subcommand passthrough, no filtering (e.g., cargo fmt → Other) - Passthrough, - /// RTK doesn't handle this command at all - NotSupported, -} - -impl RtkStatus { - pub fn as_str(&self) -> &'static str { - match self { - RtkStatus::Existing => "existing", - RtkStatus::Passthrough => "passthrough", - RtkStatus::NotSupported => "not-supported", - } - } -} - -/// A supported command that RTK already handles. -#[derive(Debug, Serialize)] -pub struct SupportedEntry { - pub command: String, - pub count: usize, - pub rtk_equivalent: &'static str, - pub category: &'static str, - pub estimated_savings_tokens: usize, - pub estimated_savings_pct: f64, - pub rtk_status: RtkStatus, -} - -/// An unsupported command not yet handled by RTK. -#[derive(Debug, Serialize)] -pub struct UnsupportedEntry { - pub base_command: String, - pub count: usize, - pub example: String, -} - -/// Full discover report. -#[derive(Debug, Serialize)] -pub struct DiscoverReport { - pub sessions_scanned: usize, - pub total_commands: usize, - pub already_rtk: usize, - pub since_days: u64, - pub supported: Vec, - pub unsupported: Vec, - pub parse_errors: usize, - pub rtk_disabled_count: usize, - pub rtk_disabled_examples: Vec, -} - -impl DiscoverReport { - pub fn total_saveable_tokens(&self) -> usize { - self.supported - .iter() - .map(|s| s.estimated_savings_tokens) - .sum() - } - - pub fn total_supported_count(&self) -> usize { - self.supported.iter().map(|s| s.count).sum() - } -} - -/// Format report as text. -pub fn format_text(report: &DiscoverReport, limit: usize, verbose: bool) -> String { - let mut out = String::with_capacity(2048); - - out.push_str("RTK Discover -- Savings Opportunities\n"); - out.push_str(&"=".repeat(52)); - out.push('\n'); - out.push_str(&format!( - "Scanned: {} sessions (last {} days), {} Bash commands\n", - report.sessions_scanned, report.since_days, report.total_commands - )); - out.push_str(&format!( - "Already using RTK: {} commands ({:.1}%)\n", - report.already_rtk, - if report.total_commands > 0 { - report.already_rtk as f64 * 100.0 / report.total_commands as f64 - } else { - 0.0 - } - )); - - if report.supported.is_empty() && report.unsupported.is_empty() { - out.push_str("\nNo missed savings found. RTK usage looks good!\n"); - return out; - } - - // Missed savings - if !report.supported.is_empty() { - out.push_str("\nMISSED SAVINGS -- Commands RTK already handles\n"); - out.push_str(&"-".repeat(72)); - out.push('\n'); - out.push_str(&format!( - "{:<24} {:>5} {:<18} {:<13} {:>12}\n", - "Command", "Count", "RTK Equivalent", "Status", "Est. Savings" - )); - - for entry in report.supported.iter().take(limit) { - out.push_str(&format!( - "{:<24} {:>5} {:<18} {:<13} ~{}\n", - truncate_str(&entry.command, 23), - entry.count, - entry.rtk_equivalent, - entry.rtk_status.as_str(), - format_tokens(entry.estimated_savings_tokens), - )); - } - - out.push_str(&"-".repeat(72)); - out.push('\n'); - out.push_str(&format!( - "Total: {} commands -> ~{} saveable\n", - report.total_supported_count(), - format_tokens(report.total_saveable_tokens()), - )); - } - - // Unhandled - if !report.unsupported.is_empty() { - out.push_str("\nTOP UNHANDLED COMMANDS -- open an issue?\n"); - out.push_str(&"-".repeat(52)); - out.push('\n'); - out.push_str(&format!( - "{:<24} {:>5} {}\n", - "Command", "Count", "Example" - )); - - for entry in report.unsupported.iter().take(limit) { - out.push_str(&format!( - "{:<24} {:>5} {}\n", - truncate_str(&entry.base_command, 23), - entry.count, - truncate_str(&entry.example, 40), - )); - } - - out.push_str(&"-".repeat(52)); - out.push('\n'); - out.push_str("-> github.com/rtk-ai/rtk/issues\n"); - } - - // RTK_DISABLED bypass warning - if report.rtk_disabled_count > 0 { - out.push_str(&format!( - "\nRTK_DISABLED BYPASS -- {} commands ran without filtering\n", - report.rtk_disabled_count - )); - out.push_str(&"-".repeat(72)); - out.push('\n'); - out.push_str("These commands used RTK_DISABLED=1 unnecessarily:\n"); - if !report.rtk_disabled_examples.is_empty() { - out.push_str(&format!(" {}\n", report.rtk_disabled_examples.join(", "))); - } - out.push_str("-> Remove RTK_DISABLED=1 to recover token savings\n"); - } - - out.push_str("\n~estimated from tool_result output sizes\n"); - - // Cursor note: check if Cursor hooks are installed - if let Some(home) = dirs::home_dir() { - let cursor_hook = home - .join(CURSOR_DIR) - .join(HOOKS_SUBDIR) - .join(REWRITE_HOOK_FILE); - if cursor_hook.exists() { - out.push_str("\nNote: Cursor sessions are tracked via `rtk gain` (discover scans Claude Code only)\n"); - } - } - - if verbose && report.parse_errors > 0 { - out.push_str(&format!("Parse errors skipped: {}\n", report.parse_errors)); - } - - out -} - -/// Format report as JSON. -pub fn format_json(report: &DiscoverReport) -> String { - serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string()) -} - -fn format_tokens(tokens: usize) -> String { - if tokens >= 1_000_000 { - format!("{:.1}M tokens", tokens as f64 / 1_000_000.0) - } else if tokens >= 1_000 { - format!("{:.1}K tokens", tokens as f64 / 1_000.0) - } else { - format!("{} tokens", tokens) - } -} - -fn truncate_str(s: &str, max: usize) -> String { - if s.len() <= max { - s.to_string() - } else { - // UTF-8 safe truncation: collect chars up to max-2, then add ".." - let truncated: String = s - .char_indices() - .take_while(|(i, _)| *i < max.saturating_sub(2)) - .map(|(_, c)| c) - .collect(); - format!("{}..", truncated) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_report(total_commands: usize, already_rtk: usize) -> DiscoverReport { - DiscoverReport { - sessions_scanned: 1, - total_commands, - already_rtk, - since_days: 30, - supported: vec![], - unsupported: vec![], - parse_errors: 0, - rtk_disabled_count: 0, - rtk_disabled_examples: vec![], - } - } - - // B6 regression: integer division truncated small percentages to 0%. - // Example: 3/1000 = 0% (old bug), should be "0.3%". - #[test] - fn test_already_rtk_percent_shows_decimal() { - let report = make_report(1000, 3); - let output = format_text(&report, 10, false); - // "0.3%" must appear; old code would print "0%" - assert!( - output.contains("0.3%"), - "Expected '0.3%' in output but got:\n{}", - output - ); - assert!( - !output.contains("(0%)"), - "Output must not contain '(0%)' — integer division bug still present:\n{}", - output - ); - } - - // Edge case: 0/0 must not divide-by-zero. - #[test] - fn test_already_rtk_percent_zero_total() { - let report = make_report(0, 0); - let output = format_text(&report, 10, false); - assert!(output.contains("0 commands (0.0%)")); - } - - // Full percent: 1000/1000 = 100.0% - #[test] - fn test_already_rtk_percent_full() { - let report = make_report(1000, 1000); - let output = format_text(&report, 10, false); - assert!(output.contains("100.0%")); - } -} diff --git a/src/discover/rules.rs b/src/discover/rules.rs index 74c876a1e..f0953984b 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -1,4 +1,4 @@ -use super::report::RtkStatus; +use super::status::RtkStatus; pub struct RtkRule { pub pattern: &'static str, diff --git a/src/discover/status.rs b/src/discover/status.rs new file mode 100644 index 000000000..bca6d718c --- /dev/null +++ b/src/discover/status.rs @@ -0,0 +1,10 @@ +//! Rewrite support status used by the registry. + +/// RTK support status for a command. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RtkStatus { + /// Dedicated handler with filtering. + Existing, + /// Works via passthrough, no filtering. + Passthrough, +} diff --git a/src/hooks/hook_audit_cmd.rs b/src/hooks/hook_audit_cmd.rs index 2138ec939..5fe339b94 100644 --- a/src/hooks/hook_audit_cmd.rs +++ b/src/hooks/hook_audit_cmd.rs @@ -143,7 +143,7 @@ pub fn run(since_days: u64, verbose: u8) -> Result<()> { if !skip_actions.is_empty() { let mut sorted_skips = skip_actions; - sorted_skips.sort_by(|a, b| b.1.cmp(&a.1)); + sorted_skips.sort_by_key(|b| std::cmp::Reverse(b.1)); for (action, count) in &sorted_skips { let reason = action.strip_prefix("skip:").unwrap_or(action); println!( diff --git a/src/hooks/hook_check.rs b/src/hooks/hook_check.rs index ce288ba99..6755072d9 100644 --- a/src/hooks/hook_check.rs +++ b/src/hooks/hook_check.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; const CURRENT_HOOK_VERSION: u8 = 3; const WARN_INTERVAL_SECS: u64 = 24 * 3600; -/// Hook status for diagnostics and `rtk gain`. +/// Hook status for diagnostics. #[derive(Debug, PartialEq, Clone)] pub enum HookStatus { /// Hook is installed and up to date. diff --git a/src/hooks/init.rs b/src/hooks/init.rs index c6bd05c2b..cb16a17f1 100644 --- a/src/hooks/init.rs +++ b/src/hooks/init.rs @@ -196,9 +196,6 @@ rtk wget # Compact download output (65%) ### Meta Commands ```bash -rtk gain # View token savings statistics -rtk gain --history # View command history with savings -rtk discover # Analyze Claude Code sessions for missed RTK usage rtk proxy # Run command without filtering (for debugging) rtk init # Add RTK instructions to CLAUDE.md rtk init --global # Add RTK to ~/.claude/CLAUDE.md @@ -297,8 +294,6 @@ pub fn run( install_cursor_hooks(verbose)?; } - prompt_telemetry_consent()?; - println!(); Ok(()) @@ -388,66 +383,6 @@ fn prompt_user_consent(settings_path: &Path) -> Result { Ok(response == "y" || response == "yes") } -pub fn save_telemetry_consent(accepted: bool) -> Result<()> { - let mut config = crate::core::config::Config::load().unwrap_or_default(); - config.telemetry.consent_given = Some(accepted); - config.telemetry.enabled = accepted; - config.telemetry.consent_date = Some(chrono::Utc::now().to_rfc3339()); - config - .save() - .context("Failed to save telemetry consent to config.toml") -} - -fn prompt_telemetry_consent() -> Result<()> { - use std::io::{self, BufRead, IsTerminal}; - - let config = crate::core::config::Config::load().unwrap_or_default(); - match config.telemetry.consent_given { - Some(true) => return Ok(()), - Some(false) => return Ok(()), - None => {} - } - - if !io::stdin().is_terminal() { - return Ok(()); - } - - eprintln!(); - eprintln!("--- Telemetry ---"); - eprintln!("RTK collects anonymous usage metrics once per day to improve filters."); - eprintln!(); - eprintln!(" What: command names (not arguments), token savings, OS, version"); - eprintln!(" Why: prioritize filter development for the most-used commands"); - eprintln!(" Who: RTK AI Labs, contact@rtk-ai.app"); - eprintln!(" Rights: disable anytime with `rtk telemetry disable`,"); - eprintln!(" request erasure with `rtk telemetry forget`"); - eprintln!(" Details: https://github.com/rtk-ai/rtk/blob/main/docs/TELEMETRY.md"); - eprintln!(); - eprint!("Enable anonymous telemetry? [y/N] "); - - let stdin = io::stdin(); - let mut line = String::new(); - stdin - .lock() - .read_line(&mut line) - .context("Failed to read user input")?; - - let accepted = { - let response = line.trim().to_lowercase(); - response == "y" || response == "yes" - }; - - save_telemetry_consent(accepted)?; - - if accepted { - eprintln!(" Telemetry enabled. Disable anytime: rtk telemetry disable"); - } else { - eprintln!(" Telemetry disabled."); - } - - Ok(()) -} - fn print_manual_instructions(hook_command: &str, include_opencode: bool) { println!("\n MANUAL STEP: Add this to ~/.claude/settings.json:"); println!(" {{"); @@ -1202,9 +1137,7 @@ fn run_hook_only_mode( if let Some(path) = &opencode_plugin_path { println!(" OpenCode: {}", path.display()); } - println!( - " Note: No RTK.md created. Claude won't know about meta commands (gain, discover, proxy)." - ); + println!(" Note: No RTK.md created. Claude won't know about meta commands (proxy)."); // Patch settings.json with binary command let patch_result = @@ -2689,10 +2622,7 @@ kubectl get pods rtk kubectl pods ## Meta commands (use directly) ```bash -rtk gain # Token savings dashboard -rtk gain --history # Per-command savings history -rtk discover # Find missed rtk opportunities -rtk proxy # Run raw (no filtering) but track usage +rtk proxy # Run raw (no filtering) ``` "#; diff --git a/src/learn/README.md b/src/learn/README.md deleted file mode 100644 index 233dc4d49..000000000 --- a/src/learn/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Learn — CLI Correction Detection - -> See also [docs/contributing/TECHNICAL.md](../../docs/contributing/TECHNICAL.md) for the full architecture overview - -## Purpose - -Analyzes Claude Code session history to detect recurring CLI mistakes — commands that fail then get corrected by the agent. Powers the `rtk learn` command, which identifies error patterns (unknown flags, wrong paths, missing args) and can auto-generate `.claude/rules/cli-corrections.md` to prevent them. - -## Key Types - -- **`ErrorType`** — `UnknownFlag`, `CommandNotFound`, `WrongSyntax`, `WrongPath`, `MissingArg`, `PermissionDenied`, `Other(String)` -- **`CorrectionPair`** — Raw detection: wrong command + right command + error output + confidence score -- **`CorrectionRule`** — Deduplicated pattern: wrong pattern + right pattern + occurrence count + base command - -## Dependencies - -- **Uses**: `discover::provider::ClaudeProvider` (session file discovery and command extraction), `lazy_static`/`regex` (error pattern matching), `serde_json` (JSON output) -- **Used by**: `src/main.rs` (routes `rtk learn` command) - -## Detection Algorithm - -1. Extract all commands from JSONL sessions via `ClaudeProvider` -2. Scan chronologically for fail-then-succeed pairs (same base command, first has error output, second succeeds) -3. Classify the error type using regex patterns on the error output -4. Assign confidence scores based on similarity and error clarity -5. Deduplicate into rules (merge identical wrong->right patterns, count occurrences) -6. Filter by `--min-confidence` and `--min-occurrences` thresholds diff --git a/src/learn/detector.rs b/src/learn/detector.rs deleted file mode 100644 index 81ebade84..000000000 --- a/src/learn/detector.rs +++ /dev/null @@ -1,629 +0,0 @@ -//! Pattern-matches CLI errors against known correction rules. - -use lazy_static::lazy_static; -use regex::Regex; - -#[derive(Debug, Clone, PartialEq)] -pub enum ErrorType { - UnknownFlag, - CommandNotFound, - #[allow(dead_code)] - WrongSyntax, - WrongPath, - MissingArg, - PermissionDenied, - Other(String), -} - -impl ErrorType { - pub fn as_str(&self) -> &str { - match self { - ErrorType::UnknownFlag => "Unknown Flag", - ErrorType::CommandNotFound => "Command Not Found", - ErrorType::WrongSyntax => "Wrong Syntax", - ErrorType::WrongPath => "Wrong Path", - ErrorType::MissingArg => "Missing Argument", - ErrorType::PermissionDenied => "Permission Denied", - ErrorType::Other(s) => s, - } - } -} - -#[derive(Debug, Clone)] -pub struct CorrectionPair { - pub wrong_command: String, - pub right_command: String, - pub error_output: String, - pub error_type: ErrorType, - pub confidence: f64, -} - -#[derive(Debug, Clone)] -pub struct CorrectionRule { - pub wrong_pattern: String, - pub right_pattern: String, - pub error_type: ErrorType, - pub occurrences: usize, - pub base_command: String, - pub example_error: String, -} - -lazy_static! { - static ref UNKNOWN_FLAG_RE: Regex = Regex::new( - r"(?i)(unexpected argument|unknown (option|flag)|unrecognized (option|flag)|invalid (option|flag))" - ).unwrap(); - - static ref CMD_NOT_FOUND_RE: Regex = Regex::new( - r"(?i)(command not found|not recognized as an internal|no such file or directory.*command)" - ).unwrap(); - - static ref WRONG_PATH_RE: Regex = Regex::new( - r"(?i)(no such file or directory|cannot find the path|file not found)" - ).unwrap(); - - static ref MISSING_ARG_RE: Regex = Regex::new( - r"(?i)(requires a value|requires an argument|missing (required )?argument|expected.*argument)" - ).unwrap(); - - static ref PERMISSION_DENIED_RE: Regex = Regex::new( - r"(?i)(permission denied|access denied|not permitted)" - ).unwrap(); - - // User rejection patterns - NOT actual errors - static ref USER_REJECTION_RE: Regex = Regex::new( - r"(?i)(user (doesn't want|declined|rejected|cancelled)|operation (cancelled|aborted) by user)" - ).unwrap(); -} - -/// Filters out user rejections - requires actual error-indicating content -pub fn is_command_error(is_error: bool, output: &str) -> bool { - if !is_error { - return false; - } - - // Reject if it's a user rejection - if USER_REJECTION_RE.is_match(output) { - return false; - } - - // Must contain error-indicating content - let output_lower = output.to_lowercase(); - output_lower.contains("error") - || output_lower.contains("failed") - || output_lower.contains("unknown") - || output_lower.contains("invalid") - || output_lower.contains("not found") - || output_lower.contains("permission denied") - || output_lower.contains("cannot") -} - -pub fn classify_error(output: &str) -> ErrorType { - if UNKNOWN_FLAG_RE.is_match(output) { - ErrorType::UnknownFlag - } else if CMD_NOT_FOUND_RE.is_match(output) { - ErrorType::CommandNotFound - } else if MISSING_ARG_RE.is_match(output) { - ErrorType::MissingArg - } else if PERMISSION_DENIED_RE.is_match(output) { - ErrorType::PermissionDenied - } else if WRONG_PATH_RE.is_match(output) { - ErrorType::WrongPath - } else { - ErrorType::Other("General Error".to_string()) - } -} - -/// Represents a command with its execution result for correction detection -pub struct CommandExecution { - pub command: String, - pub is_error: bool, - pub output: String, -} - -const CORRECTION_WINDOW: usize = 3; -const MIN_CONFIDENCE: f64 = 0.6; - -/// Extract base command (first 1-2 tokens, stripping env prefixes) -pub fn extract_base_command(cmd: &str) -> String { - let trimmed = cmd.trim(); - - // Strip common env prefixes - let stripped = trimmed - .strip_prefix("RUST_BACKTRACE=1 ") - .or_else(|| trimmed.strip_prefix("NODE_ENV=production ")) - .or_else(|| trimmed.strip_prefix("DEBUG=* ")) - .unwrap_or(trimmed); - - // Get first 1-2 tokens - let parts: Vec<&str> = stripped.split_whitespace().collect(); - match parts.len() { - 0 => String::new(), - 1 => parts[0].to_string(), - _ => format!("{} {}", parts[0], parts[1]), - } -} - -/// Calculate similarity between two commands using Jaccard similarity -/// Same base command = 0.5 base score + up to 0.5 from argument similarity -pub fn command_similarity(a: &str, b: &str) -> f64 { - let base_a = extract_base_command(a); - let base_b = extract_base_command(b); - - if base_a != base_b { - return 0.0; - } - - // Extract args (everything after base command) - let args_a: std::collections::HashSet<&str> = a - .strip_prefix(&base_a) - .unwrap_or("") - .split_whitespace() - .collect(); - - let args_b: std::collections::HashSet<&str> = b - .strip_prefix(&base_b) - .unwrap_or("") - .split_whitespace() - .collect(); - - if args_a.is_empty() && args_b.is_empty() { - return 1.0; // Identical commands - } - - let intersection = args_a.intersection(&args_b).count(); - let union = args_a.union(&args_b).count(); - - if union == 0 { - return 0.5; // Same base, no args - } - - // 0.5 for same base + up to 0.5 for arg similarity - 0.5 + (intersection as f64 / union as f64) * 0.5 -} - -/// Check if error is a compilation/test error (TDD cycle, not CLI correction) -fn is_tdd_cycle_error(error_type: &ErrorType, output: &str) -> bool { - // Compilation errors - if output.contains("error[E") || output.contains("aborting due to") { - return true; - } - - // Test failures - if output.contains("test result: FAILED") || output.contains("tests failed") { - return true; - } - - // Only syntax errors are CLI corrections - matches!(error_type, ErrorType::CommandNotFound | ErrorType::Other(_)) - && (output.contains("error[E") || output.contains("FAILED")) -} - -/// Check if commands differ only by path (exploration, not correction) -fn differs_only_by_path(a: &str, b: &str) -> bool { - let base_a = extract_base_command(a); - let base_b = extract_base_command(b); - - if base_a != base_b { - return false; - } - - // Simple heuristic: if similarity is very high (>0.9) but not identical, - // likely just path differences - let sim = command_similarity(a, b); - sim > 0.9 && sim < 1.0 -} - -pub fn find_corrections(commands: &[CommandExecution]) -> Vec { - let mut corrections = Vec::new(); - - for i in 0..commands.len() { - let cmd = &commands[i]; - - // Must be an actual error - if !is_command_error(cmd.is_error, &cmd.output) { - continue; - } - - let error_type = classify_error(&cmd.output); - - // Skip TDD cycle errors - if is_tdd_cycle_error(&error_type, &cmd.output) { - continue; - } - - // Look ahead for correction within CORRECTION_WINDOW - for candidate in commands.iter().skip(i + 1).take(CORRECTION_WINDOW) { - let similarity = command_similarity(&cmd.command, &candidate.command); - - // Must meet minimum similarity - if similarity < 0.5 { - continue; - } - - // Skip if only path differs (exploration) - if differs_only_by_path(&cmd.command, &candidate.command) { - continue; - } - - // Skip if identical commands (same error repeated) - if cmd.command == candidate.command { - continue; - } - - // Calculate confidence - let mut confidence = similarity; - - // Boost confidence if correction succeeded - if !is_command_error(candidate.is_error, &candidate.output) { - confidence = (confidence + 0.2).min(1.0); - } - - // Must meet minimum confidence - if confidence < MIN_CONFIDENCE { - continue; - } - - // Found a correction! - corrections.push(CorrectionPair { - wrong_command: cmd.command.clone(), - right_command: candidate.command.clone(), - error_output: cmd.output.chars().take(500).collect(), - error_type: error_type.clone(), - confidence, - }); - - // Take first match only - break; - } - } - - corrections -} - -/// Extract the specific token that changed between wrong and right commands -fn extract_diff_token(wrong: &str, right: &str) -> String { - let wrong_parts: std::collections::HashSet<&str> = wrong.split_whitespace().collect(); - let right_parts: std::collections::HashSet<&str> = right.split_whitespace().collect(); - - // Find tokens in wrong but not in right (removed) - let removed: Vec<&str> = wrong_parts.difference(&right_parts).copied().collect(); - - // Find tokens in right but not in wrong (added) - let added: Vec<&str> = right_parts.difference(&wrong_parts).copied().collect(); - - // Return the most distinctive change - if !removed.is_empty() && !added.is_empty() { - format!("{} → {}", removed[0], added[0]) - } else if !removed.is_empty() { - format!("removed {}", removed[0]) - } else if !added.is_empty() { - format!("added {}", added[0]) - } else { - "unknown".to_string() - } -} - -pub fn deduplicate_corrections(pairs: Vec) -> Vec { - use std::collections::HashMap; - - let mut groups: HashMap<(String, String, String), Vec> = HashMap::new(); - - // Group by (base_command, error_type, diff_token) - for pair in pairs { - let base = extract_base_command(&pair.wrong_command); - let error_type_str = pair.error_type.as_str().to_string(); - let diff_token = extract_diff_token(&pair.wrong_command, &pair.right_command); - - let key = (base, error_type_str, diff_token); - groups.entry(key).or_default().push(pair); - } - - // For each group, keep the best confidence example - let mut rules = Vec::new(); - for ((base_command, _error_type_str, _diff_token), mut group) in groups { - // Sort by confidence descending - group.sort_by(|a, b| { - b.confidence - .partial_cmp(&a.confidence) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - let best = &group[0]; - let occurrences = group.len(); - - // Reconstruct ErrorType from string (simplified - just use first one) - let error_type = best.error_type.clone(); - - rules.push(CorrectionRule { - wrong_pattern: best.wrong_command.clone(), - right_pattern: best.right_command.clone(), - error_type, - occurrences, - base_command, - example_error: best.error_output.clone(), - }); - } - - // Sort by occurrences descending (most common mistakes first) - rules.sort_by(|a, b| b.occurrences.cmp(&a.occurrences)); - - rules -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_command_error_requires_error_flag() { - assert!(!is_command_error(false, "error: unknown flag")); - assert!(is_command_error(true, "error: unknown flag")); - } - - #[test] - fn test_is_command_error_filters_user_rejection() { - assert!(!is_command_error(true, "The user doesn't want to proceed")); - assert!(!is_command_error(true, "Operation cancelled by user")); - assert!(is_command_error(true, "error: permission denied")); - } - - #[test] - fn test_is_command_error_requires_error_content() { - assert!(!is_command_error(true, "All good, success!")); - assert!(is_command_error(true, "error: something failed")); - assert!(is_command_error(true, "unknown flag --foo")); - assert!(is_command_error(true, "invalid option")); - } - - #[test] - fn test_classify_error_unknown_flag() { - assert_eq!( - classify_error("error: unexpected argument '--foo'"), - ErrorType::UnknownFlag - ); - assert_eq!( - classify_error("unknown option: --bar"), - ErrorType::UnknownFlag - ); - assert_eq!( - classify_error("unrecognized flag: -x"), - ErrorType::UnknownFlag - ); - } - - #[test] - fn test_classify_error_command_not_found() { - assert_eq!( - classify_error("bash: foobar: command not found"), - ErrorType::CommandNotFound - ); - assert_eq!( - classify_error("'xyz' is not recognized as an internal or external command"), - ErrorType::CommandNotFound - ); - } - - #[test] - fn test_classify_error_all_types() { - assert_eq!( - classify_error("No such file or directory: foo.txt"), - ErrorType::WrongPath - ); - assert_eq!( - classify_error("error: --output requires a value"), - ErrorType::MissingArg - ); - assert_eq!( - classify_error("permission denied: /etc/shadow"), - ErrorType::PermissionDenied - ); - assert!(matches!( - classify_error("something went wrong"), - ErrorType::Other(_) - )); - } - - #[test] - fn test_extract_base_command() { - assert_eq!(extract_base_command("git commit"), "git commit"); - assert_eq!(extract_base_command("cargo test"), "cargo test"); - assert_eq!( - extract_base_command("git commit --amend -m 'fix'"), - "git commit" - ); - assert_eq!( - extract_base_command("RUST_BACKTRACE=1 cargo test"), - "cargo test" - ); - } - - #[test] - fn test_command_similarity_same_base() { - assert_eq!(command_similarity("git commit", "git commit"), 1.0); - assert_eq!(command_similarity("git status", "npm install"), 0.0); - let sim = command_similarity("git commit --amend", "git commit --ammend"); - // Debug: check what similarity actually is - println!("Similarity: {}", sim); - // Same base (0.5) + both have 1 arg, 0 intersection = 0.5 + 0 = 0.5 - assert_eq!(sim, 0.5); - } - - #[test] - fn test_find_corrections_basic() { - let commands = vec![ - CommandExecution { - command: "git commit --ammend".to_string(), - is_error: true, - output: "error: unexpected argument '--ammend'".to_string(), - }, - CommandExecution { - command: "git commit --amend".to_string(), - is_error: false, - output: "[main abc123] Fix bug".to_string(), - }, - ]; - - let corrections = find_corrections(&commands); - assert_eq!(corrections.len(), 1); - assert_eq!(corrections[0].wrong_command, "git commit --ammend"); - assert_eq!(corrections[0].right_command, "git commit --amend"); - assert!(corrections[0].confidence >= 0.6); - } - - #[test] - fn test_find_corrections_window_limit() { - let commands = vec![ - CommandExecution { - command: "git commit --ammend".to_string(), - is_error: true, - output: "error: unexpected argument '--ammend'".to_string(), - }, - CommandExecution { - command: "ls".to_string(), - is_error: false, - output: "file1.txt\nfile2.txt".to_string(), - }, - CommandExecution { - command: "pwd".to_string(), - is_error: false, - output: "/home/user".to_string(), - }, - CommandExecution { - command: "echo test".to_string(), - is_error: false, - output: "test".to_string(), - }, - // Outside CORRECTION_WINDOW (3) - CommandExecution { - command: "git commit --amend".to_string(), - is_error: false, - output: "[main abc123] Fix".to_string(), - }, - ]; - - let corrections = find_corrections(&commands); - assert_eq!(corrections.len(), 0); // Too far apart - } - - #[test] - fn test_find_corrections_excludes_tdd_cycle() { - let commands = vec![ - CommandExecution { - command: "cargo test".to_string(), - is_error: true, - output: "error[E0425]: cannot find value `x`\ntest result: FAILED".to_string(), - }, - CommandExecution { - command: "cargo test".to_string(), - is_error: false, - output: "test result: ok. 5 passed".to_string(), - }, - ]; - - let corrections = find_corrections(&commands); - assert_eq!(corrections.len(), 0); // TDD cycle, not CLI correction - } - - #[test] - fn test_find_corrections_path_exploration() { - let commands = vec![ - CommandExecution { - command: "cat file1.txt".to_string(), - is_error: true, - output: "cat: file1.txt: No such file or directory".to_string(), - }, - CommandExecution { - command: "cat file2.txt".to_string(), - is_error: false, - output: "content here".to_string(), - }, - ]; - - let corrections = find_corrections(&commands); - // Should be filtered as path exploration (differs_only_by_path) - // Actually, this should NOT be filtered since base commands differ enough - // Let me adjust: they have same base "cat" but different args - assert_eq!(corrections.len(), 0); // Different files = exploration - } - - #[test] - fn test_find_corrections_min_confidence() { - let commands = vec![ - CommandExecution { - command: "git commit --foo --bar --baz".to_string(), - is_error: true, - output: "error: unexpected argument '--foo'".to_string(), - }, - CommandExecution { - command: "git commit --qux".to_string(), - is_error: false, - output: "[main abc123] Fix".to_string(), - }, - ]; - - let corrections = find_corrections(&commands); - // Similarity = 0.5 (same base) + 0 (no arg overlap) = 0.5 - // With success boost: 0.5 + 0.2 = 0.7, which passes MIN_CONFIDENCE - // So we expect 1 correction (this is a valid correction despite different args) - assert_eq!(corrections.len(), 1); - } - - #[test] - fn test_deduplicate_corrections_merges_same() { - let pairs = vec![ - CorrectionPair { - wrong_command: "git commit --ammend".to_string(), - right_command: "git commit --amend".to_string(), - error_output: "error: unexpected argument '--ammend'".to_string(), - error_type: ErrorType::UnknownFlag, - confidence: 0.8, - }, - CorrectionPair { - wrong_command: "git commit --ammend -m 'fix'".to_string(), - right_command: "git commit --amend -m 'fix'".to_string(), - error_output: "error: unexpected argument '--ammend'".to_string(), - error_type: ErrorType::UnknownFlag, - confidence: 0.9, - }, - CorrectionPair { - wrong_command: "git commit --ammend".to_string(), - right_command: "git commit --amend".to_string(), - error_output: "error: unexpected argument '--ammend'".to_string(), - error_type: ErrorType::UnknownFlag, - confidence: 0.7, - }, - ]; - - let rules = deduplicate_corrections(pairs); - assert_eq!(rules.len(), 1); // Merged into single rule - assert_eq!(rules[0].occurrences, 3); - assert_eq!(rules[0].base_command, "git commit"); - // Should keep highest confidence example (0.9) - assert!(rules[0].wrong_pattern.contains("'fix'")); - } - - #[test] - fn test_deduplicate_corrections_keeps_distinct() { - let pairs = vec![ - CorrectionPair { - wrong_command: "git commit --ammend".to_string(), - right_command: "git commit --amend".to_string(), - error_output: "error: unexpected argument '--ammend'".to_string(), - error_type: ErrorType::UnknownFlag, - confidence: 0.8, - }, - CorrectionPair { - wrong_command: "git push --force".to_string(), - right_command: "git push --force-with-lease".to_string(), - error_output: "error: --force is dangerous".to_string(), - error_type: ErrorType::WrongSyntax, - confidence: 0.7, - }, - ]; - - let rules = deduplicate_corrections(pairs); - assert_eq!(rules.len(), 2); // Different base commands and errors - assert_eq!(rules[0].occurrences, 1); - assert_eq!(rules[1].occurrences, 1); - } -} diff --git a/src/learn/mod.rs b/src/learn/mod.rs deleted file mode 100644 index 28db65cf0..000000000 --- a/src/learn/mod.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Watches for repeated CLI mistakes in coding sessions and suggests corrections. - -pub mod detector; -pub mod report; - -use crate::discover::provider::{ClaudeProvider, SessionProvider}; -use anyhow::Result; -use detector::{deduplicate_corrections, find_corrections, CommandExecution}; -use report::{format_console_report, write_rules_file}; - -pub fn run( - project: Option, - all: bool, - since: u64, - format: String, - write_rules: bool, - min_confidence: f64, - min_occurrences: usize, -) -> Result<()> { - let provider = ClaudeProvider; - - // Determine project filter (same logic as discover) - let project_filter = if all { - None - } else if let Some(p) = project { - Some(p) - } else { - // Default: current working directory - let cwd = std::env::current_dir()?; - let cwd_str = cwd.to_string_lossy().to_string(); - let encoded = ClaudeProvider::encode_project_path(&cwd_str); - Some(encoded) - }; - - // Discover sessions - let sessions = provider.discover_sessions(project_filter.as_deref(), Some(since))?; - - if sessions.is_empty() { - println!("No Claude Code sessions found in the last {} days.", since); - return Ok(()); - } - - // Extract commands from all sessions - let mut all_commands: Vec = Vec::new(); - - for session_path in &sessions { - let extracted = match provider.extract_commands(session_path) { - Ok(cmds) => cmds, - Err(_) => continue, // Skip malformed sessions - }; - - for ext_cmd in extracted { - // Only process commands with output content - if let Some(output) = ext_cmd.output_content { - all_commands.push(CommandExecution { - command: ext_cmd.command, - is_error: ext_cmd.is_error, - output, - }); - } - } - } - - // Sort by sequence index to maintain chronological order - // (already sorted by extraction order within each session) - - // Find corrections - let corrections = find_corrections(&all_commands); - - if corrections.is_empty() { - println!( - "No CLI corrections detected in {} sessions.", - sessions.len() - ); - return Ok(()); - } - - // Filter by confidence - let filtered: Vec<_> = corrections - .into_iter() - .filter(|c| c.confidence >= min_confidence) - .collect(); - - // Deduplicate - let mut rules = deduplicate_corrections(filtered.clone()); - - // Filter by occurrences - rules.retain(|r| r.occurrences >= min_occurrences); - - // Output - match format.as_str() { - "json" => { - // JSON output - let json = serde_json::json!({ - "sessions_scanned": sessions.len(), - "total_corrections": filtered.len(), - "rules": rules.iter().map(|r| serde_json::json!({ - "wrong": r.wrong_pattern, - "right": r.right_pattern, - "error_type": r.error_type.as_str(), - "occurrences": r.occurrences, - "base_command": r.base_command, - })).collect::>(), - }); - println!("{}", serde_json::to_string_pretty(&json)?); - } - _ => { - // Text output - let report = format_console_report(&rules, filtered.len(), sessions.len(), since); - print!("{}", report); - - if write_rules && !rules.is_empty() { - let rules_path = ".claude/rules/cli-corrections.md"; - write_rules_file(&rules, rules_path)?; - println!("\nWritten to: {}", rules_path); - } - } - } - - Ok(()) -} diff --git a/src/learn/report.rs b/src/learn/report.rs deleted file mode 100644 index 6ec0b442c..000000000 --- a/src/learn/report.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Formats and persists correction suggestions for the user. - -use crate::learn::detector::CorrectionRule; -use anyhow::Result; -use std::collections::HashMap; -use std::fs; -use std::path::Path; - -pub fn format_console_report( - rules: &[CorrectionRule], - total_corrections: usize, - sessions: usize, - days: u64, -) -> String { - let mut output = String::new(); - - output.push_str(&format!( - "RTK Learn -- {} rules from {} corrections ({} sessions, {} days)\n", - rules.len(), - total_corrections, - sessions, - days - )); - - if rules.is_empty() { - output.push_str("\nNo CLI corrections detected.\n"); - return output; - } - - output.push('\n'); - - for rule in rules { - let count_marker = if rule.occurrences > 1 { - format!("[{}x] ", rule.occurrences) - } else { - " ".to_string() - }; - - output.push_str(&format!( - "{}{} → {}\n", - count_marker, rule.wrong_pattern, rule.right_pattern - )); - - // Show error snippet (first line only) - let error_line = rule.example_error.lines().next().unwrap_or("").trim(); - if !error_line.is_empty() { - output.push_str(&format!(" Error: {}\n", error_line)); - } - } - - output -} - -pub fn write_rules_file(rules: &[CorrectionRule], path: &str) -> Result<()> { - let path_obj = Path::new(path); - - // Create parent directory if it doesn't exist - if let Some(parent) = path_obj.parent() { - fs::create_dir_all(parent)?; - } - - let mut content = String::new(); - content.push_str("# CLI Corrections (auto-generated by rtk learn)\n"); - content.push_str("# Run `rtk learn --write-rules` to update\n\n"); - - if rules.is_empty() { - content.push_str("No CLI corrections detected yet.\n"); - fs::write(path, content)?; - return Ok(()); - } - - // Group by base command - let mut grouped: HashMap> = HashMap::new(); - for rule in rules { - grouped - .entry(rule.base_command.clone()) - .or_default() - .push(rule); - } - - // Sort base commands alphabetically - let mut base_commands: Vec = grouped.keys().cloned().collect(); - base_commands.sort(); - - for base_cmd in base_commands { - let rules_for_cmd = grouped.get(&base_cmd).unwrap(); - - // Capitalize first letter for section header - let section_header = capitalize_first(&base_cmd); - content.push_str(&format!("## {}\n", section_header)); - - for rule in rules_for_cmd { - let occurrence_note = if rule.occurrences > 1 { - format!(" (seen {}x)", rule.occurrences) - } else { - String::new() - }; - - content.push_str(&format!( - "- Use `{}` not `{}`{}\n", - rule.right_pattern, rule.wrong_pattern, occurrence_note - )); - } - - content.push('\n'); - } - - fs::write(path, content)?; - Ok(()) -} - -fn capitalize_first(s: &str) -> String { - let mut chars = s.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::learn::detector::ErrorType; - - #[test] - fn test_format_console_report_empty() { - let report = format_console_report(&[], 0, 0, 30); - assert!(report.contains("0 rules")); - assert!(report.contains("0 corrections")); - assert!(report.contains("No CLI corrections detected")); - } - - #[test] - fn test_format_console_report_with_rules() { - let rules = vec![ - CorrectionRule { - wrong_pattern: "git commit --ammend".to_string(), - right_pattern: "git commit --amend".to_string(), - error_type: ErrorType::UnknownFlag, - occurrences: 3, - base_command: "git commit".to_string(), - example_error: "error: unexpected argument '--ammend'".to_string(), - }, - CorrectionRule { - wrong_pattern: "gh pr edit -t".to_string(), - right_pattern: "gh pr edit --title".to_string(), - error_type: ErrorType::UnknownFlag, - occurrences: 1, - base_command: "gh pr".to_string(), - example_error: "unknown flag: -t".to_string(), - }, - ]; - - let report = format_console_report(&rules, 4, 10, 30); - assert!(report.contains("2 rules")); - assert!(report.contains("4 corrections")); - assert!(report.contains("[3x]")); - assert!(report.contains("--ammend")); - assert!(report.contains("--amend")); - assert!(report.contains("Error: error: unexpected argument")); - } - - #[test] - fn test_write_rules_file_markdown() { - let rules = vec![CorrectionRule { - wrong_pattern: "git commit --ammend".to_string(), - right_pattern: "git commit --amend".to_string(), - error_type: ErrorType::UnknownFlag, - occurrences: 3, - base_command: "git commit".to_string(), - example_error: "error: unexpected argument '--ammend'".to_string(), - }]; - - let temp_dir = tempfile::tempdir().unwrap(); - let path = temp_dir.path().join("cli-corrections.md"); - let path_str = path.to_str().unwrap(); - - write_rules_file(&rules, path_str).unwrap(); - - let content = fs::read_to_string(&path).unwrap(); - assert!(content.contains("# CLI Corrections")); - assert!(content.contains("## Git commit")); - assert!(content.contains("Use `git commit --amend` not `git commit --ammend`")); - assert!(content.contains("(seen 3x)")); - } -} diff --git a/src/main.rs b/src/main.rs index ccb096ef0..0305fb1bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ mod cmds; mod core; mod discover; mod hooks; -mod learn; mod parser; // Re-export command modules for routing @@ -392,9 +391,9 @@ enum Commands { args: Vec, }, - /// Show token savings summary and history + /// Show local token savings summary and history Gain { - /// Filter statistics to current project (current working directory) // added + /// Filter statistics to current project (current working directory) #[arg(short, long)] project: bool, /// Show ASCII graph of daily savings @@ -435,25 +434,6 @@ enum Commands { yes: bool, }, - /// Claude Code economics: spending (ccusage) vs savings (rtk) analysis - CcEconomics { - /// Show detailed daily breakdown - #[arg(short, long)] - daily: bool, - /// Show weekly breakdown - #[arg(short, long)] - weekly: bool, - /// Show monthly breakdown - #[arg(short, long)] - monthly: bool, - /// Show all time breakdowns (daily + weekly + monthly) - #[arg(short, long)] - all: bool, - /// Output format: text, json, csv - #[arg(short, long, default_value = "text")] - format: String, - }, - /// Show or create configuration file Config { /// Create default config file @@ -550,59 +530,6 @@ enum Commands { args: Vec, }, - /// Discover missed RTK savings from Claude Code history - Discover { - /// Filter by project path (substring match) - #[arg(short, long)] - project: Option, - /// Max commands per section - #[arg(short, long, default_value = "15")] - limit: usize, - /// Scan all projects (default: current project only) - #[arg(short, long)] - all: bool, - /// Limit to sessions from last N days - #[arg(short, long, default_value = "30")] - since: u64, - /// Output format: text, json - #[arg(short, long, default_value = "text")] - format: String, - }, - - /// Show RTK adoption across Claude Code sessions - Session {}, - - /// Manage telemetry consent and data (RGPD/GDPR) - Telemetry { - #[command(subcommand)] - command: core::telemetry_cmd::TelemetrySubcommand, - }, - - /// Learn CLI corrections from Claude Code error history - Learn { - /// Filter by project path (substring match) - #[arg(short, long)] - project: Option, - /// Scan all projects (default: current project only) - #[arg(short, long)] - all: bool, - /// Limit to sessions from last N days - #[arg(short, long, default_value = "30")] - since: u64, - /// Output format: text, json - #[arg(short, long, default_value = "text")] - format: String, - /// Generate .claude/rules/cli-corrections.md file - #[arg(short, long)] - write_rules: bool, - /// Minimum confidence threshold (0.0-1.0) - #[arg(long, default_value = "0.6")] - min_confidence: f64, - /// Minimum occurrences to include in report - #[arg(long, default_value = "1")] - min_occurrences: usize, - }, - /// Execute a shell command via sh -c (raw, no filtering or tracking) Run { /// Command string to execute (use -c for shell-like invocation) @@ -1096,8 +1023,6 @@ enum GoCommands { /// If Clap fails to parse these, show the Clap error directly. const RTK_META_COMMANDS: &[&str] = &[ "gain", - "discover", - "learn", "init", "config", "proxy", @@ -1105,11 +1030,9 @@ const RTK_META_COMMANDS: &[&str] = &[ "hook", "hook-audit", "pipe", - "cc-economics", "verify", "trust", "untrust", - "session", "rewrite", ]; @@ -1122,7 +1045,6 @@ fn run_fallback(parse_error: clap::Error) -> Result { } // RTK meta-commands should never fall back to raw execution. - // e.g. `rtk gain --badtypo` should show Clap's error, not try to run `gain` from $PATH. if RTK_META_COMMANDS.contains(&args[0].as_str()) { parse_error.exit(); } @@ -1336,9 +1258,6 @@ fn main() { } fn run_cli() -> Result { - // Fire-and-forget telemetry ping (1/day, non-blocking) - core::telemetry::maybe_ping(); - let cli = match Cli::try_parse() { Ok(cli) => cli, Err(e) => { @@ -1350,13 +1269,10 @@ fn run_cli() -> Result { }; // Warn if installed hook is outdated/missing (1/day, non-blocking). - // Skip for Gain — it shows its own inline hook warning. - if !matches!(cli.command, Commands::Gain { .. }) { - hooks::hook_check::maybe_warn(); - } + hooks::hook_check::maybe_warn(); // Runtime integrity check for operational commands. - // Meta commands (init, gain, verify, config, etc.) skip the check + // Meta commands (init, verify, config, etc.) skip the check // because they don't go through the hook pipeline. if is_operational_command(&cli.command) { hooks::integrity::runtime_check()?; @@ -1832,7 +1748,7 @@ fn run_cli() -> Result { Commands::Wc { args } => wc_cmd::run(&args, cli.verbose)?, Commands::Gain { - project, // added + project, graph, history, quota, @@ -1847,7 +1763,7 @@ fn run_cli() -> Result { yes, } => { analytics::gain::run( - project, // added: pass project flag + project, graph, history, quota, @@ -1865,17 +1781,6 @@ fn run_cli() -> Result { 0 } - Commands::CcEconomics { - daily, - weekly, - monthly, - all, - format, - } => { - analytics::cc_economics::run(daily, weekly, monthly, all, &format, cli.verbose)?; - 0 - } - Commands::Config { create } => { if create { let path = core::config::Config::create_default()?; @@ -1960,48 +1865,6 @@ fn run_cli() -> Result { Commands::Curl { args } => curl_cmd::run(&args, cli.verbose)?, - Commands::Discover { - project, - limit, - all, - since, - format, - } => { - discover::run(project.as_deref(), all, since, limit, &format, cli.verbose)?; - 0 - } - - Commands::Session {} => { - analytics::session_cmd::run(cli.verbose)?; - 0 - } - - Commands::Telemetry { command } => { - core::telemetry_cmd::run(&command)?; - 0 - } - - Commands::Learn { - project, - all, - since, - format, - write_rules, - min_confidence, - min_occurrences, - } => { - learn::run( - project, - all, - since, - format, - write_rules, - min_confidence, - min_occurrences, - )?; - 0 - } - Commands::Npx { args } => { if args.is_empty() { anyhow::bail!("npx requires a command argument"); @@ -2379,7 +2242,7 @@ fn run_cli() -> Result { /// Returns true for commands that are invoked via the hook pipeline /// (i.e., commands that process rewritten shell commands). -/// Meta commands (init, gain, verify, etc.) are excluded because +/// Meta commands (init, verify, etc.) are excluded because /// they are run directly by the user, not through the hook. /// Returns true for commands that go through the hook pipeline /// and therefore require integrity verification. @@ -2617,30 +2480,6 @@ mod tests { } } - #[test] - fn test_gain_failures_flag_parses() { - let result = Cli::try_parse_from(["rtk", "gain", "--failures"]); - assert!(result.is_ok()); - if let Ok(cli) = result { - match cli.command { - Commands::Gain { failures, .. } => assert!(failures), - _ => panic!("Expected Gain command"), - } - } - } - - #[test] - fn test_gain_failures_short_flag_parses() { - let result = Cli::try_parse_from(["rtk", "gain", "-F"]); - assert!(result.is_ok()); - if let Ok(cli) = result { - match cli.command { - Commands::Gain { failures, .. } => assert!(failures), - _ => panic!("Expected Gain command"), - } - } - } - #[test] fn test_meta_commands_reject_bad_flags() { // RTK meta-commands should produce parse errors (not fall through to raw execution). @@ -2728,14 +2567,11 @@ mod tests { // Verify all meta-commands are in the guard list by checking they parse with valid syntax let meta_cmds_that_parse = [ vec!["rtk", "gain"], - vec!["rtk", "discover"], - vec!["rtk", "learn"], vec!["rtk", "init"], vec!["rtk", "config"], vec!["rtk", "proxy", "echo", "hi"], vec!["rtk", "run", "-c", "echo hi"], vec!["rtk", "hook-audit"], - vec!["rtk", "cc-economics"], ]; for args in &meta_cmds_that_parse { let result = Cli::try_parse_from(args.iter()); diff --git a/src/parser/README.md b/src/parser/README.md index a0ae36f81..95789f6fc 100644 --- a/src/parser/README.md +++ b/src/parser/README.md @@ -132,6 +132,5 @@ Run `cargo test parser::tests`. Each parser should have tier validation tests: a - [ ] gh_cmd.rs → GhParser ### Phase 5: Observability -- [ ] Extend tracking.db: `parse_tier`, `format_mode` - [ ] `rtk parse-health` command - [ ] Alert if degradation > 10%