Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

__version__ = "2.0.1"
__version__ = "2.0.2"


def data_dir() -> str:
Expand Down
163 changes: 128 additions & 35 deletions src/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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():
Expand All @@ -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__":
Expand Down
33 changes: 33 additions & 0 deletions src/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,39 @@ 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:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
46 changes: 41 additions & 5 deletions tests/test_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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()
Expand Down
Loading