mr-claude-stats 1.0.3 → 1.3.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,19 +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)
93
92
 
94
93
  ## Compatibility
95
94
 
96
- | OS | Status | Notes |
97
- |----|--------|-------|
98
- | Linux | ✅ Works | Native support |
99
- | macOS | ✅ Works | Uses `tail -r` instead of `tac` |
100
- | Windows | ⚠️ WSL/Git Bash | Requires bash environment |
95
+ | OS | Status |
96
+ |----|--------|
97
+ | Linux | ✅ Native |
98
+ | macOS | ✅ Native |
99
+ | Windows | Native |
101
100
 
102
- **macOS**: Install jq with `brew install jq`
103
- **Windows**: Use WSL or Git Bash, install jq
101
+ **No additional dependencies!** Pure Node.js, no bash or jq needed.
104
102
 
105
103
  ## License
106
104
 
@@ -0,0 +1,198 @@
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 path = require('path');
7
+ const readline = require('readline');
8
+
9
+ const VERSION = '1.3.0';
10
+ const BAR_SIZE = 45;
11
+
12
+ // Handle --help and --version
13
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
14
+ console.log(`mr-claude-stats - The most accurate Claude Code statusline
15
+
16
+ INSTALL:
17
+ npm install -g mr-claude-stats
18
+
19
+ SETUP:
20
+ Add to ~/.claude/settings.json:
21
+
22
+ {
23
+ "statusLine": {
24
+ "type": "command",
25
+ "command": "mr-claude-stats"
26
+ }
27
+ }
28
+
29
+ WHAT IT SHOWS:
30
+ ████████████████████████░░░░░░░░░░░░░░░░░░░
31
+ Opus 4.5 in /lasy 130k/200k (65%)
32
+
33
+ WORKS ON:
34
+ Linux, macOS, Windows (native!)
35
+
36
+ MORE INFO:
37
+ https://github.com/MrIago/mr-claude-stats`);
38
+ process.exit(0);
39
+ }
40
+
41
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
42
+ console.log(`mr-claude-stats v${VERSION}`);
43
+ process.exit(0);
44
+ }
45
+
46
+ // ANSI colors (pastel)
47
+ const BLUE = '\x1b[38;5;117m';
48
+ const GREEN = '\x1b[38;5;114m';
49
+ const YELLOW = '\x1b[38;5;186m';
50
+ const ORANGE = '\x1b[38;5;216m';
51
+ const RED = '\x1b[38;5;174m';
52
+ const GRAY = '\x1b[38;5;242m';
53
+ const RESET = '\x1b[0m';
54
+
55
+ function formatTokens(n) {
56
+ return n >= 1000 ? Math.floor(n / 1000) + 'k' : String(n);
57
+ }
58
+
59
+ function getColorForPercent(percent) {
60
+ if (percent < 25) return GREEN;
61
+ if (percent < 50) return YELLOW;
62
+ if (percent < 75) return ORANGE;
63
+ return RED;
64
+ }
65
+
66
+ function buildProgressBar(percent, size = BAR_SIZE) {
67
+ const filled = Math.floor(percent * size / 100);
68
+ const empty = size - filled;
69
+
70
+ const t1 = Math.floor(size * 0.25);
71
+ const t2 = Math.floor(size * 0.50);
72
+ const t3 = Math.floor(size * 0.75);
73
+
74
+ let bar = '';
75
+ for (let i = 0; i < filled; i++) {
76
+ if (i < t1) bar += GREEN + '█';
77
+ else if (i < t2) bar += YELLOW + '█';
78
+ else if (i < t3) bar += ORANGE + '█';
79
+ else bar += RED + '█';
80
+ }
81
+ bar += GRAY + '░'.repeat(empty) + RESET;
82
+ return bar;
83
+ }
84
+
85
+ function getCacheFile(sessionId) {
86
+ const os = require('os');
87
+ return path.join(os.tmpdir(), `statusline_cache_${sessionId || 'default'}`);
88
+ }
89
+
90
+ function readCache(sessionId) {
91
+ try {
92
+ const cacheFile = getCacheFile(sessionId);
93
+ if (fs.existsSync(cacheFile)) {
94
+ return parseInt(fs.readFileSync(cacheFile, 'utf8').trim()) || 0;
95
+ }
96
+ } catch (e) {}
97
+ return 0;
98
+ }
99
+
100
+ function writeCache(sessionId, value) {
101
+ try {
102
+ fs.writeFileSync(getCacheFile(sessionId), String(value));
103
+ } catch (e) {}
104
+ }
105
+
106
+ function findLastUsage(transcriptPath) {
107
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
108
+
109
+ try {
110
+ const content = fs.readFileSync(transcriptPath, 'utf8');
111
+ const lines = content.trim().split('\n').reverse();
112
+
113
+ for (const line of lines) {
114
+ try {
115
+ const entry = JSON.parse(line);
116
+ const usage = entry.usage || (entry.message && entry.message.usage);
117
+ if (usage && usage.input_tokens !== undefined) {
118
+ return usage;
119
+ }
120
+ } catch (e) {}
121
+ }
122
+ } catch (e) {}
123
+ return null;
124
+ }
125
+
126
+ function truncatePath(dirName, maxLen = 10) {
127
+ if (dirName.length > maxLen) {
128
+ return dirName.substring(0, 7) + '...';
129
+ }
130
+ return dirName;
131
+ }
132
+
133
+ async function main() {
134
+ // Read JSON from stdin
135
+ const rl = readline.createInterface({ input: process.stdin });
136
+ let inputData = '';
137
+
138
+ for await (const line of rl) {
139
+ inputData += line;
140
+ }
141
+
142
+ let input;
143
+ try {
144
+ input = JSON.parse(inputData);
145
+ } catch (e) {
146
+ input = {};
147
+ }
148
+
149
+ const model = input.model?.display_name || 'Claude';
150
+ const contextSize = input.context_window?.context_window_size || 200000;
151
+ const transcriptPath = input.transcript_path || '';
152
+ const sessionId = input.session_id || 'default';
153
+ const cwd = input.cwd || process.cwd();
154
+
155
+ // Get last directory name and truncate if needed
156
+ const lastDir = truncatePath(path.basename(cwd));
157
+ const modelWithPath = `${model} in /${lastDir}`;
158
+
159
+ // Calculate total tokens
160
+ let total = 0;
161
+ const usage = findLastUsage(transcriptPath);
162
+
163
+ if (usage) {
164
+ const inputTokens = usage.input_tokens || 0;
165
+ const cacheCreate = usage.cache_creation_input_tokens || 0;
166
+ const cacheRead = usage.cache_read_input_tokens || 0;
167
+ const outputTokens = usage.output_tokens || 0;
168
+
169
+ total = inputTokens + cacheCreate + cacheRead + outputTokens;
170
+ writeCache(sessionId, total);
171
+ } else {
172
+ total = readCache(sessionId);
173
+ }
174
+
175
+ // If no data, show only model with path
176
+ if (total === 0) {
177
+ console.log(`${BLUE}${modelWithPath}${RESET}`);
178
+ return;
179
+ }
180
+
181
+ const percent = Math.floor(total * 100 / contextSize);
182
+ const textColor = getColorForPercent(percent);
183
+
184
+ // Format output
185
+ const totalFmt = formatTokens(total);
186
+ const contextFmt = formatTokens(contextSize);
187
+ const rightInfo = `${totalFmt}/${contextFmt} (${percent}%)`.padStart(18);
188
+
189
+ // Left side width = BAR_SIZE - 18 (right info width)
190
+ const leftWidth = BAR_SIZE - 18;
191
+
192
+ // Row 1: Progress bar
193
+ console.log(buildProgressBar(percent));
194
+ // Row 2: Model with path (left) + tokens (right)
195
+ console.log(`${BLUE}${modelWithPath.padEnd(leftWidth)}${RESET}${textColor}${rightInfo}${RESET}`);
196
+ }
197
+
198
+ 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",
3
+ "version": "1.3.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,158 +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
- # Usar tail -r no Mac, tac no Linux
61
- if command -v tac &>/dev/null; then
62
- LAST_USAGE=$(tac "$TRANSCRIPT" 2>/dev/null | jq -s '[.[] | select(.usage != null or .message.usage != null)] | .[0] | .usage // .message.usage' 2>/dev/null)
63
- else
64
- LAST_USAGE=$(tail -r "$TRANSCRIPT" 2>/dev/null | jq -s '[.[] | select(.usage != null or .message.usage != null)] | .[0] | .usage // .message.usage' 2>/dev/null)
65
- fi
66
- if [ -n "$LAST_USAGE" ] && [ "$LAST_USAGE" != "null" ]; then
67
- INPUT_T=$(echo "$LAST_USAGE" | jq -r '.input_tokens // 0')
68
- CACHE_CREATE=$(echo "$LAST_USAGE" | jq -r '.cache_creation_input_tokens // 0')
69
- CACHE_READ=$(echo "$LAST_USAGE" | jq -r '.cache_read_input_tokens // 0')
70
- OUTPUT_T=$(echo "$LAST_USAGE" | jq -r '.output_tokens // 0')
71
- # Total = input atual + cache + output + buffer de autocompact (45k)
72
- MESSAGES=$((INPUT_T + CACHE_CREATE + CACHE_READ + OUTPUT_T))
73
- AUTOCOMPACT_BUFFER=45000
74
- TOTAL=$((MESSAGES + AUTOCOMPACT_BUFFER))
75
- # Salvar no cache
76
- echo "$TOTAL" > "$CACHE_FILE"
77
- fi
78
- fi
79
-
80
- # Se não encontrou, usar cache anterior
81
- if [ "$TOTAL" -eq 0 ] && [ -f "$CACHE_FILE" ]; then
82
- TOTAL=$(cat "$CACHE_FILE" 2>/dev/null || echo 0)
83
- fi
84
-
85
- # Se não tem dados, mostrar só o modelo
86
- if [ "$TOTAL" -eq 0 ]; then
87
- echo -e "\033[38;5;117m${MODEL}\033[0m"
88
- exit 0
89
- fi
90
- PERCENT=$((TOTAL * 100 / CONTEXT))
91
-
92
- # Cores ANSI (pastel)
93
- BLUE='\033[38;5;117m'
94
- GREEN='\033[38;5;114m'
95
- YELLOW='\033[38;5;186m'
96
- ORANGE='\033[38;5;216m'
97
- RED='\033[38;5;174m'
98
- GRAY='\033[38;5;242m'
99
- RESET='\033[0m'
100
-
101
-
102
- # Formatar tokens
103
- format_tokens() {
104
- local n=$1
105
- if [ $n -ge 1000 ]; then
106
- echo "$((n / 1000))k"
107
- else
108
- echo "$n"
109
- fi
110
- }
111
-
112
- TOTAL_FMT=$(format_tokens $TOTAL)
113
- CONTEXT_FMT=$(format_tokens $CONTEXT)
114
-
115
- # Barra de 60 caracteres com gradiente
116
- BAR_SIZE=60
117
- FILLED=$((PERCENT * BAR_SIZE / 100))
118
- EMPTY=$((BAR_SIZE - FILLED))
119
-
120
- # Thresholds para cores (em chars)
121
- T1=$((BAR_SIZE * 25 / 100)) # 25% = 15 chars
122
- T2=$((BAR_SIZE * 50 / 100)) # 50% = 30 chars
123
- T3=$((BAR_SIZE * 75 / 100)) # 75% = 45 chars
124
-
125
- BAR=""
126
- for ((i=0; i<FILLED; i++)); do
127
- if [ $i -lt $T1 ]; then
128
- BAR+="${GREEN}█"
129
- elif [ $i -lt $T2 ]; then
130
- BAR+="${YELLOW}█"
131
- elif [ $i -lt $T3 ]; then
132
- BAR+="${ORANGE}█"
133
- else
134
- BAR+="${RED}█"
135
- fi
136
- done
137
- BAR+="${GRAY}"
138
- for ((i=0; i<EMPTY; i++)); do BAR+="░"; done
139
- BAR+="${RESET}"
140
-
141
- # Cor do texto baseada no percentual
142
- if [ $PERCENT -lt 25 ]; then
143
- TEXT_COLOR=$GREEN
144
- elif [ $PERCENT -lt 50 ]; then
145
- TEXT_COLOR=$YELLOW
146
- elif [ $PERCENT -lt 75 ]; then
147
- TEXT_COLOR=$ORANGE
148
- else
149
- TEXT_COLOR=$RED
150
- fi
151
-
152
- # Info formatada com largura fixa (18 chars total)
153
- RIGHT=$(printf "%18s" "${TOTAL_FMT}/${CONTEXT_FMT} (${PERCENT}%)")
154
-
155
- # Row 1: Barra
156
- echo -e "$BAR"
157
- # Row 2: Model (azul) esquerda, tokens (cor) direita
158
- echo -e "${BLUE}$(printf "%-42s" "$MODEL")${RESET}${TEXT_COLOR}${RIGHT}${RESET}"