mr-claude-stats 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,8 +88,17 @@ This matches the official `/context` command output.
88
88
 
89
89
  ## Requirements
90
90
 
91
- - `jq` (JSON processor)
92
- - `bash`
91
+ - Node.js 14+ (comes with npm)
92
+
93
+ ## Compatibility
94
+
95
+ | OS | Status |
96
+ |----|--------|
97
+ | Linux | ✅ Native |
98
+ | macOS | ✅ Native |
99
+ | Windows | ✅ Native |
100
+
101
+ **No additional dependencies!** Pure Node.js, no bash or jq needed.
93
102
 
94
103
  ## License
95
104
 
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ // mr-claude-stats - Accurate statusline for Claude Code CLI
3
+ // https://github.com/MrIago/mr-claude-stats
4
+
5
+ const fs = require('fs');
6
+ const readline = require('readline');
7
+
8
+ const VERSION = '1.1.0';
9
+
10
+ // Handle --help and --version
11
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
12
+ console.log(`mr-claude-stats - The most accurate Claude Code statusline
13
+
14
+ INSTALL:
15
+ npm install -g mr-claude-stats
16
+
17
+ SETUP:
18
+ Add to ~/.claude/settings.json:
19
+
20
+ {
21
+ "statusLine": {
22
+ "type": "command",
23
+ "command": "mr-claude-stats"
24
+ }
25
+ }
26
+
27
+ WHAT IT SHOWS:
28
+ ████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
29
+ Opus 4.5 130k/200k (65%)
30
+
31
+ WORKS ON:
32
+ Linux, macOS, Windows (native!)
33
+
34
+ MORE INFO:
35
+ https://github.com/MrIago/mr-claude-stats`);
36
+ process.exit(0);
37
+ }
38
+
39
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
40
+ console.log(`mr-claude-stats v${VERSION}`);
41
+ process.exit(0);
42
+ }
43
+
44
+ // ANSI colors (pastel)
45
+ const BLUE = '\x1b[38;5;117m';
46
+ const GREEN = '\x1b[38;5;114m';
47
+ const YELLOW = '\x1b[38;5;186m';
48
+ const ORANGE = '\x1b[38;5;216m';
49
+ const RED = '\x1b[38;5;174m';
50
+ const GRAY = '\x1b[38;5;242m';
51
+ const RESET = '\x1b[0m';
52
+
53
+ function formatTokens(n) {
54
+ return n >= 1000 ? Math.floor(n / 1000) + 'k' : String(n);
55
+ }
56
+
57
+ function getColorForPercent(percent) {
58
+ if (percent < 25) return GREEN;
59
+ if (percent < 50) return YELLOW;
60
+ if (percent < 75) return ORANGE;
61
+ return RED;
62
+ }
63
+
64
+ function buildProgressBar(percent, size = 60) {
65
+ const filled = Math.floor(percent * size / 100);
66
+ const empty = size - filled;
67
+
68
+ const t1 = Math.floor(size * 0.25);
69
+ const t2 = Math.floor(size * 0.50);
70
+ const t3 = Math.floor(size * 0.75);
71
+
72
+ let bar = '';
73
+ for (let i = 0; i < filled; i++) {
74
+ if (i < t1) bar += GREEN + '█';
75
+ else if (i < t2) bar += YELLOW + '█';
76
+ else if (i < t3) bar += ORANGE + '█';
77
+ else bar += RED + '█';
78
+ }
79
+ bar += GRAY + '░'.repeat(empty) + RESET;
80
+ return bar;
81
+ }
82
+
83
+ function getCacheFile(sessionId) {
84
+ const os = require('os');
85
+ return require('path').join(os.tmpdir(), `statusline_cache_${sessionId || 'default'}`);
86
+ }
87
+
88
+ function readCache(sessionId) {
89
+ try {
90
+ const cacheFile = getCacheFile(sessionId);
91
+ if (fs.existsSync(cacheFile)) {
92
+ return parseInt(fs.readFileSync(cacheFile, 'utf8').trim()) || 0;
93
+ }
94
+ } catch (e) {}
95
+ return 0;
96
+ }
97
+
98
+ function writeCache(sessionId, value) {
99
+ try {
100
+ fs.writeFileSync(getCacheFile(sessionId), String(value));
101
+ } catch (e) {}
102
+ }
103
+
104
+ function findLastUsage(transcriptPath) {
105
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
106
+
107
+ try {
108
+ const content = fs.readFileSync(transcriptPath, 'utf8');
109
+ const lines = content.trim().split('\n').reverse();
110
+
111
+ for (const line of lines) {
112
+ try {
113
+ const entry = JSON.parse(line);
114
+ const usage = entry.usage || (entry.message && entry.message.usage);
115
+ if (usage && usage.input_tokens !== undefined) {
116
+ return usage;
117
+ }
118
+ } catch (e) {}
119
+ }
120
+ } catch (e) {}
121
+ return null;
122
+ }
123
+
124
+ async function main() {
125
+ // Read JSON from stdin
126
+ const rl = readline.createInterface({ input: process.stdin });
127
+ let inputData = '';
128
+
129
+ for await (const line of rl) {
130
+ inputData += line;
131
+ }
132
+
133
+ let input;
134
+ try {
135
+ input = JSON.parse(inputData);
136
+ } catch (e) {
137
+ input = {};
138
+ }
139
+
140
+ const model = input.model?.display_name || 'Claude';
141
+ const contextSize = input.context_window?.context_window_size || 200000;
142
+ const transcriptPath = input.transcript_path || '';
143
+ const sessionId = input.session_id || 'default';
144
+
145
+ // Calculate total tokens
146
+ let total = 0;
147
+ const usage = findLastUsage(transcriptPath);
148
+
149
+ if (usage) {
150
+ const inputTokens = usage.input_tokens || 0;
151
+ const cacheCreate = usage.cache_creation_input_tokens || 0;
152
+ const cacheRead = usage.cache_read_input_tokens || 0;
153
+ const outputTokens = usage.output_tokens || 0;
154
+ const autocompactBuffer = 45000;
155
+
156
+ total = inputTokens + cacheCreate + cacheRead + outputTokens + autocompactBuffer;
157
+ writeCache(sessionId, total);
158
+ } else {
159
+ total = readCache(sessionId);
160
+ }
161
+
162
+ // If no data, show only model
163
+ if (total === 0) {
164
+ console.log(`${BLUE}${model}${RESET}`);
165
+ return;
166
+ }
167
+
168
+ const percent = Math.floor(total * 100 / contextSize);
169
+ const textColor = getColorForPercent(percent);
170
+
171
+ // Format output
172
+ const totalFmt = formatTokens(total);
173
+ const contextFmt = formatTokens(contextSize);
174
+ const info = `${totalFmt}/${contextFmt} (${percent}%)`.padStart(18);
175
+
176
+ // Row 1: Progress bar
177
+ console.log(buildProgressBar(percent));
178
+ // Row 2: Model (left) + tokens (right)
179
+ console.log(`${BLUE}${model.padEnd(42)}${RESET}${textColor}${info}${RESET}`);
180
+ }
181
+
182
+ main().catch(() => process.exit(1));
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "mr-claude-stats",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Accurate statusline for Claude Code CLI with colorful progress bar",
5
5
  "bin": {
6
- "mr-claude-stats": "./bin/mr-claude-stats"
6
+ "mr-claude-stats": "./bin/mr-claude-stats.js"
7
7
  },
8
8
  "keywords": [
9
9
  "claude",
@@ -1,153 +0,0 @@
1
- #!/bin/bash
2
- # mr-claude-stats - Accurate statusline for Claude Code CLI
3
- # https://github.com/MrIago/mr-claude-stats
4
-
5
- VERSION="1.0.2"
6
-
7
- # Handle --help and --version
8
- if [[ "$1" == "--help" || "$1" == "-h" ]]; then
9
- cat << 'EOF'
10
- mr-claude-stats - The most accurate Claude Code statusline
11
-
12
- INSTALL:
13
- npm install -g mr-claude-stats
14
-
15
- SETUP:
16
- Add to ~/.claude/settings.json:
17
-
18
- {
19
- "statusLine": {
20
- "type": "command",
21
- "command": "mr-claude-stats"
22
- }
23
- }
24
-
25
- WHAT IT SHOWS:
26
- ████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
27
- Opus 4.5 130k/200k (65%)
28
-
29
- REQUIREMENTS:
30
- - jq (JSON processor)
31
- - bash
32
-
33
- MORE INFO:
34
- https://github.com/MrIago/mr-claude-stats
35
- EOF
36
- exit 0
37
- fi
38
-
39
- if [[ "$1" == "--version" || "$1" == "-v" ]]; then
40
- echo "mr-claude-stats v$VERSION"
41
- exit 0
42
- fi
43
-
44
- export LC_NUMERIC=C
45
- input=$(cat)
46
-
47
- MODEL=$(echo "$input" | jq -r '.model.display_name // "Claude"')
48
- CONTEXT=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
49
- TRANSCRIPT=$(echo "$input" | jq -r '.transcript_path // ""')
50
-
51
- # Cache para persistir último valor conhecido
52
- CACHE_FILE="/tmp/statusline_cache_${SESSION_ID:-default}"
53
- SESSION_ID=$(echo "$input" | jq -r '.session_id // "default"')
54
- CACHE_FILE="/tmp/statusline_cache_${SESSION_ID}"
55
-
56
- # Ler usage do último request com usage válido no transcript
57
- TOTAL=0
58
- if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
59
- # Buscar último entry que tem usage (ignorar comandos locais sem usage)
60
- LAST_USAGE=$(tac "$TRANSCRIPT" 2>/dev/null | jq -s '[.[] | select(.usage != null or .message.usage != null)] | .[0] | .usage // .message.usage' 2>/dev/null)
61
- if [ -n "$LAST_USAGE" ] && [ "$LAST_USAGE" != "null" ]; then
62
- INPUT_T=$(echo "$LAST_USAGE" | jq -r '.input_tokens // 0')
63
- CACHE_CREATE=$(echo "$LAST_USAGE" | jq -r '.cache_creation_input_tokens // 0')
64
- CACHE_READ=$(echo "$LAST_USAGE" | jq -r '.cache_read_input_tokens // 0')
65
- OUTPUT_T=$(echo "$LAST_USAGE" | jq -r '.output_tokens // 0')
66
- # Total = input atual + cache + output + buffer de autocompact (45k)
67
- MESSAGES=$((INPUT_T + CACHE_CREATE + CACHE_READ + OUTPUT_T))
68
- AUTOCOMPACT_BUFFER=45000
69
- TOTAL=$((MESSAGES + AUTOCOMPACT_BUFFER))
70
- # Salvar no cache
71
- echo "$TOTAL" > "$CACHE_FILE"
72
- fi
73
- fi
74
-
75
- # Se não encontrou, usar cache anterior
76
- if [ "$TOTAL" -eq 0 ] && [ -f "$CACHE_FILE" ]; then
77
- TOTAL=$(cat "$CACHE_FILE" 2>/dev/null || echo 0)
78
- fi
79
-
80
- # Se não tem dados, mostrar só o modelo
81
- if [ "$TOTAL" -eq 0 ]; then
82
- echo -e "\033[38;5;117m${MODEL}\033[0m"
83
- exit 0
84
- fi
85
- PERCENT=$((TOTAL * 100 / CONTEXT))
86
-
87
- # Cores ANSI (pastel)
88
- BLUE='\033[38;5;117m'
89
- GREEN='\033[38;5;114m'
90
- YELLOW='\033[38;5;186m'
91
- ORANGE='\033[38;5;216m'
92
- RED='\033[38;5;174m'
93
- GRAY='\033[38;5;242m'
94
- RESET='\033[0m'
95
-
96
-
97
- # Formatar tokens
98
- format_tokens() {
99
- local n=$1
100
- if [ $n -ge 1000 ]; then
101
- echo "$((n / 1000))k"
102
- else
103
- echo "$n"
104
- fi
105
- }
106
-
107
- TOTAL_FMT=$(format_tokens $TOTAL)
108
- CONTEXT_FMT=$(format_tokens $CONTEXT)
109
-
110
- # Barra de 60 caracteres com gradiente
111
- BAR_SIZE=60
112
- FILLED=$((PERCENT * BAR_SIZE / 100))
113
- EMPTY=$((BAR_SIZE - FILLED))
114
-
115
- # Thresholds para cores (em chars)
116
- T1=$((BAR_SIZE * 25 / 100)) # 25% = 15 chars
117
- T2=$((BAR_SIZE * 50 / 100)) # 50% = 30 chars
118
- T3=$((BAR_SIZE * 75 / 100)) # 75% = 45 chars
119
-
120
- BAR=""
121
- for ((i=0; i<FILLED; i++)); do
122
- if [ $i -lt $T1 ]; then
123
- BAR+="${GREEN}█"
124
- elif [ $i -lt $T2 ]; then
125
- BAR+="${YELLOW}█"
126
- elif [ $i -lt $T3 ]; then
127
- BAR+="${ORANGE}█"
128
- else
129
- BAR+="${RED}█"
130
- fi
131
- done
132
- BAR+="${GRAY}"
133
- for ((i=0; i<EMPTY; i++)); do BAR+="░"; done
134
- BAR+="${RESET}"
135
-
136
- # Cor do texto baseada no percentual
137
- if [ $PERCENT -lt 25 ]; then
138
- TEXT_COLOR=$GREEN
139
- elif [ $PERCENT -lt 50 ]; then
140
- TEXT_COLOR=$YELLOW
141
- elif [ $PERCENT -lt 75 ]; then
142
- TEXT_COLOR=$ORANGE
143
- else
144
- TEXT_COLOR=$RED
145
- fi
146
-
147
- # Info formatada com largura fixa (18 chars total)
148
- RIGHT=$(printf "%18s" "${TOTAL_FMT}/${CONTEXT_FMT} (${PERCENT}%)")
149
-
150
- # Row 1: Barra
151
- echo -e "$BAR"
152
- # Row 2: Model (azul) esquerda, tokens (cor) direita
153
- echo -e "${BLUE}$(printf "%-42s" "$MODEL")${RESET}${TEXT_COLOR}${RIGHT}${RESET}"