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 +11 -2
- package/bin/mr-claude-stats.js +182 -0
- package/package.json +2 -2
- package/bin/mr-claude-stats +0 -153
package/README.md
CHANGED
|
@@ -88,8 +88,17 @@ This matches the official `/context` command output.
|
|
|
88
88
|
|
|
89
89
|
## Requirements
|
|
90
90
|
|
|
91
|
-
-
|
|
92
|
-
|
|
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
|
|
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",
|
package/bin/mr-claude-stats
DELETED
|
@@ -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}"
|