skill-statusline 2.3.0 → 2.3.1

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.
@@ -1,85 +1,136 @@
1
- #!/usr/bin/env node
2
- // skill-statusline v2.3 — Node.js statusline renderer
3
- // Zero bash dependency — works on Windows, macOS, Linux
4
- // Synchronous stdin read for maximum speed
5
-
6
- 'use strict';
7
- const fs = require('fs');
8
- const path = require('path');
9
- try {
10
- const input = fs.readFileSync(0, 'utf8');
11
- if (input) render(JSON.parse(input));
12
- } catch (e) { /* silent exit on any error */ }
13
-
14
- function render(data) {
15
- const RST = '\x1b[0m', BOLD = '\x1b[1m';
16
- const CYAN = '\x1b[38;2;6;182;212m', PURPLE = '\x1b[38;2;168;85;247m';
17
- const GREEN = '\x1b[38;2;34;197;94m', YELLOW = '\x1b[38;2;245;158;11m';
18
- const RED = '\x1b[38;2;239;68;68m', ORANGE = '\x1b[38;2;251;146;60m';
19
- const WHITE = '\x1b[38;2;228;228;231m', PINK = '\x1b[38;2;236;72;153m';
20
- const SEP = '\x1b[38;2;55;55;62m', DIM = '\x1b[38;2;40;40;45m';
21
- const BLUE = '\x1b[38;2;59;130;246m';
22
-
23
- // Model display_name already includes version (e.g. "Opus 4.6")
24
- const model = data.model?.display_name || 'unknown';
25
-
26
- // Directory last 3 path segments
27
- const cwd = (data.workspace?.current_dir || data.cwd || '').replace(/\\/g, '/').replace(/\/\/+/g, '/');
28
- const parts = cwd.split('/').filter(Boolean);
29
- const dir = parts.length > 3 ? parts.slice(-3).join('/') : parts.length > 0 ? parts.join('/') : '~';
30
-
31
- // Git branch — read .git/HEAD directly (no subprocess, instant)
32
- let branch = '';
33
- try {
34
- const projectDir = data.workspace?.project_dir || data.workspace?.current_dir || data.cwd || '';
35
- const gitHead = fs.readFileSync(path.join(projectDir, '.git', 'HEAD'), 'utf8').trim();
36
- branch = gitHead.startsWith('ref: refs/heads/') ? gitHead.slice(16) : gitHead.slice(0, 7);
37
- } catch (e) { branch = 'no-git'; }
38
-
39
- // Context — use provided percentage (most accurate)
40
- let pct = Math.floor(data.context_window?.used_percentage || 0);
41
- if (pct > 100) pct = 100;
42
- const ctxClr = pct > 90 ? RED : pct > 75 ? ORANGE : pct > 40 ? YELLOW : WHITE;
43
- const barW = 40;
44
- const filled = Math.min(Math.floor(pct * barW / 100), barW);
45
- const bar = ctxClr + '\u2588'.repeat(filled) + RST + DIM + '\u2591'.repeat(barW - filled) + RST;
46
-
47
- // Cost
48
- const costRaw = data.cost?.total_cost_usd || 0;
49
- const cost = costRaw === 0 ? '$0.00' : costRaw < 0.01 ? `$${costRaw.toFixed(4)}` : `$${costRaw.toFixed(2)}`;
50
-
51
- // Tokens show session totals (in / out / total)
52
- const fmtTok = n => n >= 1000000 ? `${(n/1000000).toFixed(1)}M` : n >= 1000 ? `${(n/1000).toFixed(1)}k` : `${n}`;
53
- const totIn = data.context_window?.total_input_tokens || 0;
54
- const totOut = data.context_window?.total_output_tokens || 0;
55
- const tokTotal = fmtTok(totIn + totOut);
56
- const tokIn = fmtTok(totIn);
57
- const tokOut = fmtTok(totOut);
58
-
59
- // Session duration
60
- const durMs = data.cost?.total_duration_ms || 0;
61
- const durMin = Math.floor(durMs / 60000);
62
- const durSec = Math.floor((durMs % 60000) / 1000);
63
- const duration = durMin > 0 ? `${durMin}m ${durSec}s` : `${durSec}s`;
64
-
65
- // Lines changed
66
- const linesAdded = data.cost?.total_lines_added || 0;
67
- const linesRemoved = data.cost?.total_lines_removed || 0;
68
-
69
- // Separator + padding
70
- const S = ` ${SEP}\u2502${RST} `;
71
- const rpad = (s, w) => {
72
- const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
73
- return s + (plain.length < w ? ' '.repeat(w - plain.length) : '');
74
- };
75
- const C1 = 44;
76
-
77
- // Output 4 rows
78
- let out = '';
79
- out += ' ' + rpad(`${PURPLE}Model:${RST} ${PURPLE}${BOLD}${model}${RST}`, C1) + S + `${CYAN}Dir:${RST} ${CYAN}${dir}${RST}\n`;
80
- out += ' ' + rpad(`${YELLOW}Tokens:${RST} ${YELLOW}${tokIn} in + ${tokOut} out = ${BOLD}${tokTotal}${RST}`, C1) + S + `${GREEN}Cost:${RST} ${GREEN}${cost}${RST}\n`;
81
- out += ' ' + rpad(`${BLUE}Session:${RST} ${BLUE}${duration}${RST} ${DIM}|${RST} ${GREEN}+${linesAdded}${RST}${DIM}/${RST}${RED}-${linesRemoved}${RST} ${DIM}lines${RST}`, C1) + S + `${WHITE}Git:${RST} ${WHITE}${branch}${RST}\n`;
82
- out += ` ${ctxClr}Context:${RST} ${bar} ${ctxClr}${pct}%${RST}`;
83
-
84
- process.stdout.write(out);
85
- }
1
+ #!/usr/bin/env node
2
+ // skill-statusline v2.3 — Node.js statusline renderer
3
+ // Zero bash dependency — works on Windows, macOS, Linux
4
+ // Synchronous stdin read + transcript tail for live activity
5
+
6
+ 'use strict';
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ try {
10
+ const input = fs.readFileSync(0, 'utf8');
11
+ if (input) render(JSON.parse(input));
12
+ } catch (e) { /* silent exit on any error */ }
13
+
14
+ function getActivity(transcriptPath) {
15
+ try {
16
+ const stat = fs.statSync(transcriptPath);
17
+ const readSize = Math.min(16384, stat.size);
18
+ const buf = Buffer.alloc(readSize);
19
+ const fd = fs.openSync(transcriptPath, 'r');
20
+ fs.readSync(fd, buf, 0, readSize, Math.max(0, stat.size - readSize));
21
+ fs.closeSync(fd);
22
+ const lines = buf.toString('utf8').split('\n').filter(l => l.trim());
23
+ for (let i = lines.length - 1; i >= 0; i--) {
24
+ try {
25
+ const entry = JSON.parse(lines[i]);
26
+ if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
27
+ const toolUses = entry.message.content.filter(c => c.type === 'tool_use');
28
+ if (toolUses.length) {
29
+ const last = toolUses[toolUses.length - 1];
30
+ const name = last.name;
31
+ const inp = last.input || {};
32
+ // Enrich with context for special tools
33
+ if (name === 'Task' && inp.subagent_type) {
34
+ const desc = inp.description ? ': ' + inp.description.slice(0, 25) : '';
35
+ return `Task(${inp.subagent_type}${desc})`;
36
+ }
37
+ if (name === 'Skill' && inp.skill) return `Skill(${inp.skill})`;
38
+ return name;
39
+ }
40
+ }
41
+ } catch (e) { continue; }
42
+ }
43
+ } catch (e) { /* ignore */ }
44
+ return 'Idle';
45
+ }
46
+
47
+ function getGitInfo(projectDir) {
48
+ let branch = '', remote = '';
49
+ try {
50
+ const gitHead = fs.readFileSync(path.join(projectDir, '.git', 'HEAD'), 'utf8').trim();
51
+ branch = gitHead.startsWith('ref: refs/heads/') ? gitHead.slice(16) : gitHead.slice(0, 7);
52
+ } catch (e) { return 'no-git'; }
53
+ // Parse remote URL from .git/config
54
+ try {
55
+ const config = fs.readFileSync(path.join(projectDir, '.git', 'config'), 'utf8');
56
+ const urlMatch = config.match(/\[remote "origin"\][^[]*url\s*=\s*(.+)/);
57
+ if (urlMatch) {
58
+ const url = urlMatch[1].trim();
59
+ // Extract owner/repo from various URL formats
60
+ const ghMatch = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
61
+ if (ghMatch) remote = ghMatch[1] + '/' + ghMatch[2];
62
+ }
63
+ } catch (e) { /* ignore */ }
64
+ return remote ? `${remote}:${branch}` : branch;
65
+ }
66
+
67
+ function render(data) {
68
+ const RST = '\x1b[0m', BOLD = '\x1b[1m';
69
+ const CYAN = '\x1b[38;2;6;182;212m', PURPLE = '\x1b[38;2;168;85;247m';
70
+ const GREEN = '\x1b[38;2;34;197;94m', YELLOW = '\x1b[38;2;245;158;11m';
71
+ const RED = '\x1b[38;2;239;68;68m', ORANGE = '\x1b[38;2;251;146;60m';
72
+ const WHITE = '\x1b[38;2;228;228;231m', PINK = '\x1b[38;2;236;72;153m';
73
+ const SEP = '\x1b[38;2;55;55;62m', DIM = '\x1b[38;2;40;40;45m';
74
+ const BLUE = '\x1b[38;2;59;130;246m';
75
+
76
+ // Model
77
+ const model = data.model?.display_name || 'unknown';
78
+
79
+ // Directory last 3 path segments
80
+ const cwd = (data.workspace?.current_dir || data.cwd || '').replace(/\\/g, '/').replace(/\/\/+/g, '/');
81
+ const parts = cwd.split('/').filter(Boolean);
82
+ const dir = parts.length > 3 ? parts.slice(-3).join('/') : parts.length > 0 ? parts.join('/') : '~';
83
+
84
+ // Git — account/repo:branch from .git/HEAD + .git/config
85
+ const projectDir = data.workspace?.project_dir || data.workspace?.current_dir || data.cwd || '';
86
+ const gitInfo = getGitInfo(projectDir);
87
+
88
+ // Live activity — tail transcript for current tool/skill/agent/task
89
+ const activity = getActivity(data.transcript_path);
90
+
91
+ // Context — use Claude's provided percentage
92
+ let pct = Math.floor(data.context_window?.used_percentage || 0);
93
+ if (pct > 100) pct = 100;
94
+ const ctxClr = pct > 90 ? RED : pct > 75 ? ORANGE : pct > 40 ? YELLOW : WHITE;
95
+ const barW = 40;
96
+ const filled = Math.min(Math.floor(pct * barW / 100), barW);
97
+ const bar = ctxClr + '\u2588'.repeat(filled) + RST + DIM + '\u2591'.repeat(barW - filled) + RST;
98
+
99
+ // Cost
100
+ const costRaw = data.cost?.total_cost_usd || 0;
101
+ const cost = costRaw === 0 ? '$0.00' : costRaw < 0.01 ? `$${costRaw.toFixed(4)}` : `$${costRaw.toFixed(2)}`;
102
+
103
+ // Tokens — session totals (in + out = total)
104
+ const fmtTok = n => n >= 1000000 ? `${(n/1000000).toFixed(1)}M` : n >= 1000 ? `${(n/1000).toFixed(1)}k` : `${n}`;
105
+ const totIn = data.context_window?.total_input_tokens || 0;
106
+ const totOut = data.context_window?.total_output_tokens || 0;
107
+ const tokTotal = fmtTok(totIn + totOut);
108
+ const tokIn = fmtTok(totIn);
109
+ const tokOut = fmtTok(totOut);
110
+
111
+ // Session duration
112
+ const durMs = data.cost?.total_duration_ms || 0;
113
+ const durMin = Math.floor(durMs / 60000);
114
+ const durSec = Math.floor((durMs % 60000) / 1000);
115
+ const duration = durMin > 0 ? `${durMin}m ${durSec}s` : `${durSec}s`;
116
+
117
+ // Activity color — highlight when active
118
+ const actClr = activity === 'Idle' ? DIM : GREEN;
119
+
120
+ // Separator + padding
121
+ const S = ` ${SEP}\u2502${RST} `;
122
+ const rpad = (s, w) => {
123
+ const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
124
+ return s + (plain.length < w ? ' '.repeat(w - plain.length) : '');
125
+ };
126
+ const C1 = 44;
127
+
128
+ // Output 4 rows
129
+ let out = '';
130
+ out += ' ' + rpad(`${actClr}Action:${RST} ${actClr}${activity}${RST}`, C1) + S + `${WHITE}Git:${RST} ${WHITE}${gitInfo}${RST}\n`;
131
+ out += ' ' + rpad(`${PURPLE}Model:${RST} ${PURPLE}${BOLD}${model}${RST}`, C1) + S + `${CYAN}Dir:${RST} ${CYAN}${dir}${RST}\n`;
132
+ out += ' ' + rpad(`${YELLOW}Tokens:${RST} ${YELLOW}${tokIn} ${WHITE}in${RST} ${YELLOW}+ ${tokOut} ${WHITE}out${RST} ${YELLOW}= ${BOLD}${tokTotal}${RST}`, C1) + S + `${GREEN}Cost:${RST} ${GREEN}${cost}${RST}\n`;
133
+ out += ' ' + rpad(`${BLUE}Session:${RST} ${BLUE}${duration}${RST}`, C1) + S + `${ctxClr}Context:${RST} ${bar} ${ctxClr}${pct}%${RST}`;
134
+
135
+ process.stdout.write(out);
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-statusline",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display. Pure bash, zero deps.",
5
5
  "bin": {
6
6
  "skill-statusline": "bin/cli.js",