From 16039baca45e4b6d5f854a999e48d68b149e2893 Mon Sep 17 00:00:00 2001 From: Philippe Granger <90652303+ppgranger@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:45:16 +0100 Subject: [PATCH 1/3] feat: redesign stats visualization with ANSI colors and by-command breakdown (v2.0.2) - Add get_top_commands() method to tracker for per-command savings breakdown - Redesign stats output with colored header, efficiency progress bar, and impact bars - Add top_commands to JSON output for scripting - Bump version to 2.0.2 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- src/__init__.py | 2 +- src/stats.py | 163 +++++++++++++++++++++++++------- src/tracker.py | 31 ++++++ tests/test_tracker.py | 46 ++++++++- 6 files changed, 203 insertions(+), 43 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6fad2b8..8992331 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "token-saver", "source": "./", "description": "Automatically compresses verbose CLI output to save tokens. Supports git, docker, npm, terraform, kubectl, and 13+ other command families.", - "version": "2.0.1", + "version": "2.0.2", "author": { "name": "ppgranger" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 999c512..b1422dc 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "token-saver", "description": "Automatically compresses verbose CLI output (git, docker, npm, terraform, kubectl, etc.) to save tokens in Claude Code sessions. Supports 18+ command families with smart compression.", - "version": "2.0.1", + "version": "2.0.2", "author": { "name": "ppgranger", "url": "https://github.com/ppgranger" diff --git a/src/__init__.py b/src/__init__.py index 941397a..f489d68 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,6 @@ import os -__version__ = "2.0.1" +__version__ = "2.0.2" def data_dir() -> str: diff --git a/src/stats.py b/src/stats.py index 5b94f06..e94048f 100644 --- a/src/stats.py +++ b/src/stats.py @@ -14,6 +14,21 @@ from src.tracker import SavingsTracker +# ── ANSI escape codes ────────────────────────────────────────────── +BOLD = "\033[1m" +DIM = "\033[2m" +RESET = "\033[0m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +RED = "\033[31m" +CYAN = "\033[36m" +WHITE = "\033[97m" +BOLD_GREEN = "\033[1;32m" +BOLD_WHITE = "\033[1;97m" +BOLD_YELLOW = "\033[1;33m" + +WIDTH = 50 + def _chars_to_tokens(n: int) -> int: """Estimate token count from character count.""" @@ -25,10 +40,99 @@ def _chars_to_tokens(n: int) -> int: def _format_tokens(n: int) -> str: """Human-readable token count.""" if n < 1_000: - return f"{n} tokens" + return f"{n}" if n < 1_000_000: - return f"{n / 1_000:.1f}k tokens" - return f"{n / 1_000_000:.1f}M tokens" + return f"{n / 1_000:.1f}K" + return f"{n / 1_000_000:.1f}M" + + +def _ratio_color(ratio: float) -> str: + """Return ANSI color code based on compression ratio.""" + if ratio >= 60: + return GREEN + if ratio >= 30: + return YELLOW + return RED + + +def _progress_bar(ratio: float, width: int = 20) -> str: + """Render a progress bar with filled/empty blocks.""" + filled = round(ratio / 100 * width) + empty = width - filled + return f"{CYAN}{'█' * filled}{'░' * empty}{RESET}" + + +def _impact_bar(value: float, max_value: float, width: int = 10) -> str: + """Render an impact bar proportional to max value.""" + if max_value <= 0: + return "" + filled = max(1, round(value / max_value * width)) + return f"{CYAN}{'█' * filled}{RESET}" + + +def _print_header(): + print() + print(f" {BOLD_GREEN}Token-Saver Savings (Lifetime){RESET}") + print(f" {BOLD_YELLOW}{'═' * WIDTH}{RESET}") + + +def _print_summary(lifetime): + orig_tokens = _chars_to_tokens(lifetime["original"]) + comp_tokens = _chars_to_tokens(lifetime["compressed"]) + saved_tokens = _chars_to_tokens(lifetime["saved"]) + ratio = lifetime["ratio"] + color = _ratio_color(ratio) + + print() + print(f" {'Total commands:':<20s} {BOLD_WHITE}{lifetime['commands']}{RESET}") + print(f" {'Input tokens:':<20s} {BOLD_WHITE}{_format_tokens(orig_tokens)}{RESET}") + print(f" {'Output tokens:':<20s} {BOLD_WHITE}{_format_tokens(comp_tokens)}{RESET}") + print( + f" {'Tokens saved:':<20s} {BOLD_WHITE}{_format_tokens(saved_tokens)}{RESET}" + f" {color}({ratio}%){RESET}" + ) + print(f" {'Efficiency:':<20s} {_progress_bar(ratio)} {color}{ratio}%{RESET}") + + +def _print_by_command(top_commands): + if not top_commands: + return + + print() + print(f" {BOLD_GREEN}By Command{RESET}") + print(f" {BOLD_YELLOW}{'─' * WIDTH}{RESET}") + print() + + # Header row + print( + f" {DIM}{'#':>3s}{RESET} " + f"{'Command':<20s} " + f"{'Count':>5s} " + f"{'Saved':>6s} " + f"{'Avg%':>5s} " + f"Impact" + ) + + max_saved = top_commands[0]["total_saved"] if top_commands else 1 + cmd_width = 20 + + for i, cmd in enumerate(top_commands, 1): + saved_tokens = _chars_to_tokens(cmd["total_saved"]) + ratio = cmd["avg_ratio"] + color = _ratio_color(ratio) + bar = _impact_bar(cmd["total_saved"], max_saved) + name = cmd["command"][:cmd_width].ljust(cmd_width) + + print( + f" {DIM}{i:>3d}.{RESET} " + f"{CYAN}{name}{RESET} " + f"{cmd['count']:>5d} " + f"{BOLD_WHITE}{_format_tokens(saved_tokens):>6s}{RESET} " + f"{color}{ratio:>5.1f}%{RESET} " + f"{bar}" + ) + + print() def main(): @@ -49,47 +153,36 @@ def main(): tracker = SavingsTracker(session_id=session_id) session = tracker.get_session_stats() lifetime = tracker.get_lifetime_stats() - top = tracker.get_top_processors(limit=5) + top_processors = tracker.get_top_processors(limit=5) + top_commands = tracker.get_top_commands(limit=10) tracker.close() if as_json: - json.dump({"session": session, "lifetime": lifetime, "top_processors": top}, sys.stdout) + json.dump( + { + "session": session, + "lifetime": lifetime, + "top_processors": top_processors, + "top_commands": top_commands, + }, + sys.stdout, + ) sys.stdout.write("\n") return # --- Human-readable output --- - print("Token-Saver Statistics") - print("=" * 40) - - print("\nSession") - print("-" * 40) - if session["commands"] == 0: - print(" No compressions in this session.") - else: - print(f" Commands compressed: {session['commands']}") - print(f" Original tokens: {_format_tokens(_chars_to_tokens(session['original']))}") - print(f" Compressed tokens: {_format_tokens(_chars_to_tokens(session['compressed']))}") - saved = _format_tokens(_chars_to_tokens(session["saved"])) - print(f" Saved: {saved} ({session['ratio']}%)") - - print("\nLifetime") - print("-" * 40) if lifetime["commands"] == 0: + print() + print(f" {BOLD_GREEN}Token-Saver Savings{RESET}") + print(f" {BOLD_YELLOW}{'═' * WIDTH}{RESET}") + print() print(" No compressions recorded yet.") - else: - print(f" Sessions: {lifetime['sessions']}") - print(f" Commands compressed: {lifetime['commands']}") - print(f" Original tokens: {_format_tokens(_chars_to_tokens(lifetime['original']))}") - print(f" Compressed tokens: {_format_tokens(_chars_to_tokens(lifetime['compressed']))}") - saved = _format_tokens(_chars_to_tokens(lifetime["saved"])) - print(f" Saved: {saved} ({lifetime['ratio']}%)") - - if top: - print("\nTop Processors") - print("-" * 40) - for entry in top: - saved = _format_tokens(_chars_to_tokens(entry["saved"])) - print(f" {entry['processor']:<20s} {entry['count']:>4d} cmds, {saved} saved") + print() + return + + _print_header() + _print_summary(lifetime) + _print_by_command(top_commands) if __name__ == "__main__": diff --git a/src/tracker.py b/src/tracker.py index 0ff3f3e..0b692fc 100644 --- a/src/tracker.py +++ b/src/tracker.py @@ -224,6 +224,37 @@ def get_lifetime_stats(self) -> dict: "ratio": round(ratio, 1), } + def get_top_commands(self, limit: int = 10) -> list[dict]: + """Get commands with the most token savings.""" + with self._lock: + rows = self.conn.execute( + """ + SELECT command, + COUNT(*) as count, + SUM(original_size) as total_original, + SUM(compressed_size) as total_compressed, + SUM(original_size - compressed_size) as total_saved + FROM savings + GROUP BY command + ORDER BY total_saved DESC + LIMIT ? + """, + (limit,), + ).fetchall() + results = [] + for r in rows: + orig = r["total_original"] + ratio = ((orig - r["total_compressed"]) / orig * 100) if orig > 0 else 0.0 + results.append({ + "command": r["command"], + "count": r["count"], + "total_original": orig, + "total_compressed": r["total_compressed"], + "total_saved": r["total_saved"], + "avg_ratio": round(ratio, 1), + }) + return results + def get_top_processors(self, limit: int = 5) -> list[dict]: """Get the most effective processors.""" with self._lock: diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 07d7f53..c779f19 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -120,6 +120,36 @@ def test_top_processors(self): assert len(top) == 2 assert top[0]["processor"] == "git" # More saved + def test_top_commands_grouping_and_order(self): + """get_top_commands groups by command and orders by total_saved DESC.""" + self.tracker.record_saving("git status", "git", 1000, 200, "claude_code") + self.tracker.record_saving("git status", "git", 1000, 300, "claude_code") + self.tracker.record_saving("git diff", "git", 5000, 1000, "claude_code") + self.tracker.record_saving("pytest", "test", 500, 100, "claude_code") + top = self.tracker.get_top_commands() + assert len(top) == 3 + # git diff saved 4000, git status saved 1500, pytest saved 400 + assert top[0]["command"] == "git diff" + assert top[0]["total_saved"] == 4000 + assert top[0]["count"] == 1 + assert top[1]["command"] == "git status" + assert top[1]["total_saved"] == 1500 + assert top[1]["count"] == 2 + assert top[2]["command"] == "pytest" + + def test_top_commands_limit(self): + """get_top_commands respects the limit parameter.""" + for i in range(5): + self.tracker.record_saving(f"cmd-{i}", "test", 100 * (i + 1), 10, "claude_code") + top = self.tracker.get_top_commands(limit=3) + assert len(top) == 3 + + def test_top_commands_avg_ratio(self): + """get_top_commands computes avg_ratio correctly.""" + self.tracker.record_saving("git status", "git", 1000, 200, "claude_code") + top = self.tracker.get_top_commands() + assert top[0]["avg_ratio"] == 80.0 + def test_concurrent_writes(self): """Multiple threads writing should not crash.""" errors = [] @@ -256,7 +286,7 @@ def _seed_data(self): def test_empty_db_human(self): result = self._run_stats() assert result.returncode == 0 - assert "Token-Saver Statistics" in result.stdout + assert "Token-Saver Savings" in result.stdout assert "No compressions" in result.stdout def test_empty_db_json(self): @@ -271,10 +301,10 @@ def test_with_data_human(self): self._seed_data() result = self._run_stats() assert result.returncode == 0 - assert "Lifetime" in result.stdout - assert "Commands compressed:" in result.stdout - assert "Saved:" in result.stdout - assert "Top Processors" in result.stdout + assert "Token-Saver Savings" in result.stdout + assert "Total commands:" in result.stdout + assert "Tokens saved:" in result.stdout + assert "By Command" in result.stdout assert "git" in result.stdout def test_with_data_json(self): @@ -288,6 +318,12 @@ def test_with_data_json(self): assert data["lifetime"]["saved"] == 14700 assert len(data["top_processors"]) == 2 assert data["top_processors"][0]["processor"] == "git" + # top_commands should be present + assert "top_commands" in data + assert len(data["top_commands"]) > 0 + assert "command" in data["top_commands"][0] + assert "total_saved" in data["top_commands"][0] + assert "avg_ratio" in data["top_commands"][0] def test_top_processors_order(self): self._seed_data() From b2eedce6a76fa1e4b2c4a60a739a81c7a0fc5394 Mon Sep 17 00:00:00 2001 From: Philippe Granger <90652303+ppgranger@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:47:04 +0100 Subject: [PATCH 2/3] style: apply ruff format to tracker.py --- src/tracker.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/tracker.py b/src/tracker.py index 0b692fc..ba42fb4 100644 --- a/src/tracker.py +++ b/src/tracker.py @@ -245,14 +245,16 @@ def get_top_commands(self, limit: int = 10) -> list[dict]: for r in rows: orig = r["total_original"] ratio = ((orig - r["total_compressed"]) / orig * 100) if orig > 0 else 0.0 - results.append({ - "command": r["command"], - "count": r["count"], - "total_original": orig, - "total_compressed": r["total_compressed"], - "total_saved": r["total_saved"], - "avg_ratio": round(ratio, 1), - }) + results.append( + { + "command": r["command"], + "count": r["count"], + "total_original": orig, + "total_compressed": r["total_compressed"], + "total_saved": r["total_saved"], + "avg_ratio": round(ratio, 1), + } + ) return results def get_top_processors(self, limit: int = 5) -> list[dict]: From 7fd3cfa712a71d88c266335436c447c051d3c636 Mon Sep 17 00:00:00 2001 From: Philippe Granger <90652303+ppgranger@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:47:34 +0100 Subject: [PATCH 3/3] fix: update test_cli assertion for new stats output title --- tests/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 1bd9044..a162a65 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -47,7 +47,7 @@ class TestStatsCommand: def test_stats_human_readable(self): rc, stdout, _ = _run_cli("stats") assert rc == 0 - assert "Token-Saver Statistics" in stdout + assert "Token-Saver Savings" in stdout def test_stats_json(self): rc, stdout, _ = _run_cli("stats", "--json")