diff --git a/src/dippy/cli/rtk.py b/src/dippy/cli/rtk.py new file mode 100644 index 0000000..4f3f56a --- /dev/null +++ b/src/dippy/cli/rtk.py @@ -0,0 +1,47 @@ +"""rtk (Rust Token Killer) handler for Dippy. + +rtk is a token-optimized CLI proxy that is typically prepended to bash +commands via a Claude Code PreToolUse hook. When both rtk and Dippy are +installed, Dippy sees commands like ``rtk git log`` and would otherwise +miss the ``git`` handler. This handler treats rtk as a transparent +wrapper so the inner command is analyzed directly. + +Meta subcommands that do not wrap another command: +- ``rtk gain [--history]``: print token savings analytics (read-only) +- ``rtk discover``: analyze Claude Code history (read-only) +- ``rtk proxy ``: run the raw command without rtk's filtering; we + still delegate to the inner command for safety analysis +""" + +from __future__ import annotations + +from dippy.cli import Classification, HandlerContext +from dippy.core.bash import bash_join + +COMMANDS = ["rtk"] + +READ_ONLY_SUBCOMMANDS = frozenset({"gain", "discover"}) + + +def classify(ctx: HandlerContext) -> Classification: + """Classify an rtk command.""" + tokens = ctx.tokens + if len(tokens) == 1: + return Classification("ask", description="rtk") + + sub = tokens[1] + + if sub in READ_ONLY_SUBCOMMANDS: + return Classification("allow", description=f"rtk {sub}") + + if sub == "proxy": + if len(tokens) == 2: + return Classification("ask", description="rtk proxy") + inner_cmd = bash_join(tokens[2:]) + return Classification("delegate", inner_command=inner_cmd) + + if sub.startswith("-"): + return Classification("ask", description=f"rtk {sub}") + + inner_cmd = bash_join(tokens[1:]) + return Classification("delegate", inner_command=inner_cmd) diff --git a/tests/cli/test_rtk.py b/tests/cli/test_rtk.py new file mode 100644 index 0000000..494434c --- /dev/null +++ b/tests/cli/test_rtk.py @@ -0,0 +1,52 @@ +"""Test cases for rtk (Rust Token Killer) transparent wrapper.""" + +import pytest +from conftest import is_approved, needs_confirmation + +TESTS = [ + # Bare rtk - ask + ("rtk", False), + # Version / help - handled by global version/help check + ("rtk --version", True), + ("rtk --help", True), + ("rtk -h", True), + ("rtk help", True), + ("rtk version", True), + # Read-only meta subcommands + ("rtk gain", True), + ("rtk gain --history", True), + ("rtk discover", True), + # Transparent wrapper - safe inner + ("rtk ls", True), + ("rtk ls -la", True), + ("rtk cat README.md", True), + ("rtk git status", True), + ("rtk git log", True), + ("rtk git log --oneline -5", True), + # Transparent wrapper - unsafe inner + ("rtk rm -rf /", False), + ("rtk git push --force origin main", False), + ("rtk make", False), + # proxy escape hatch - delegates to inner + ("rtk proxy ls", True), + ("rtk proxy git status", True), + ("rtk proxy rm -rf /", False), + # proxy with no inner command - ask + ("rtk proxy", False), + # Chained with && still inspects each side + ("rtk git status && rtk git log --oneline -5", True), + ("rtk ls && rtk rm foo", False), + # Piped through another rtk-wrapped command + ("rtk git log --oneline | rtk head -5", True), + # Unknown rtk flag - ask + ("rtk --nonexistent-flag", False), +] + + +@pytest.mark.parametrize("command,expected", TESTS) +def test_command(check, command: str, expected: bool): + result = check(command) + if expected: + assert is_approved(result), f"Expected approve: {command}" + else: + assert needs_confirmation(result), f"Expected confirm: {command}"