skill-statusline 2.3.0 → 2.4.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 +12 -8
- package/bin/cli.js +116 -48
- package/bin/statusline-node.js +132 -85
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# skill-statusline
|
|
2
2
|
|
|
3
|
-
Rich, themeable statusline for Claude Code with accurate context tracking, 5 color themes, 3 layout modes, and zero dependencies.
|
|
3
|
+
Rich, themeable statusline for Claude Code with accurate context tracking, 5 color themes, 3 layout modes, and zero dependencies. Uses a fast Node.js renderer on Windows (no Git Bash overhead), pure bash on Unix.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -124,13 +124,19 @@ Stored in `~/.claude/statusline-config.json`:
|
|
|
124
124
|
|
|
125
125
|
## Architecture
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Two rendering engines — the installer picks the right one for your platform:
|
|
128
|
+
|
|
129
|
+
- **Windows**: Node.js renderer (`statusline-node.js`) — single process, no subprocess spawning, ~30-50ms
|
|
130
|
+
- **macOS/Linux**: Bash engine (`core.sh`) — pure bash, zero dependencies, <50ms with caching
|
|
131
|
+
|
|
132
|
+
Git Bash on Windows has ~50-100ms overhead *per subprocess spawn* (awk, sed, grep, git, date). A bash statusline spawning 10-20 subprocesses = 500-1000ms. The Node.js renderer eliminates this by doing everything in a single process.
|
|
128
133
|
|
|
129
134
|
```
|
|
130
135
|
~/.claude/
|
|
131
|
-
statusline-command.sh #
|
|
136
|
+
statusline-command.sh # Bash entry point (macOS/Linux)
|
|
137
|
+
statusline-node.js # Node.js renderer (Windows)
|
|
132
138
|
statusline/
|
|
133
|
-
core.sh #
|
|
139
|
+
core.sh # Bash engine: parse JSON, compute fields, render
|
|
134
140
|
json-parser.sh # Nested JSON extraction (no jq)
|
|
135
141
|
helpers.sh # Utilities + filesystem caching
|
|
136
142
|
themes/{default,nord,...}.sh # Color palettes
|
|
@@ -138,15 +144,13 @@ Pure bash at runtime — zero dependencies. Node.js CLI is only for installation
|
|
|
138
144
|
statusline-config.json # User preferences
|
|
139
145
|
```
|
|
140
146
|
|
|
141
|
-
Performance: <50ms with caching, <100ms cold. Git and transcript reads are cached with configurable TTL.
|
|
142
|
-
|
|
143
147
|
Terminal width is auto-detected — layouts gracefully degrade on narrow terminals.
|
|
144
148
|
|
|
145
149
|
## Requirements
|
|
146
150
|
|
|
147
|
-
-
|
|
151
|
+
- Node.js >=16 (installer + Windows renderer)
|
|
148
152
|
- Git (for GitHub field)
|
|
149
|
-
-
|
|
153
|
+
- Bash (macOS/Linux runtime only — not needed on Windows)
|
|
150
154
|
- Works on Windows, macOS, Linux
|
|
151
155
|
|
|
152
156
|
## Part of the Thinqmesh Skills Ecosystem
|
package/bin/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const readline = require('readline');
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
const command = args[0];
|
|
11
11
|
const subcommand = args[1];
|
|
12
|
-
const VERSION = '2.
|
|
12
|
+
const VERSION = '2.4.0';
|
|
13
13
|
|
|
14
14
|
const PKG_DIR = path.resolve(__dirname, '..');
|
|
15
15
|
const HOME = os.homedir();
|
|
@@ -271,10 +271,17 @@ function installFiles() {
|
|
|
271
271
|
copyDir(layoutsSrc, path.join(SL_DIR, 'layouts'));
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
// Copy entry point
|
|
274
|
+
// Copy entry point (bash)
|
|
275
275
|
const slSrc = path.join(PKG_DIR, 'bin', 'statusline.sh');
|
|
276
276
|
fs.copyFileSync(slSrc, SCRIPT_DEST);
|
|
277
277
|
|
|
278
|
+
// Copy Node.js renderer (used on Windows for performance)
|
|
279
|
+
const nodeSrc = path.join(PKG_DIR, 'bin', 'statusline-node.js');
|
|
280
|
+
const nodeDest = path.join(CLAUDE_DIR, 'statusline-node.js');
|
|
281
|
+
if (fs.existsSync(nodeSrc)) {
|
|
282
|
+
fs.copyFileSync(nodeSrc, nodeDest);
|
|
283
|
+
}
|
|
284
|
+
|
|
278
285
|
return true;
|
|
279
286
|
}
|
|
280
287
|
|
|
@@ -352,23 +359,30 @@ async function install() {
|
|
|
352
359
|
writeConfig(config);
|
|
353
360
|
success(`Config: theme=${CYN}${config.theme}${R}, layout=${CYN}${config.layout}${R}`);
|
|
354
361
|
|
|
355
|
-
// Update settings.json
|
|
362
|
+
// Update settings.json
|
|
356
363
|
const settings = readSettings();
|
|
357
|
-
|
|
358
|
-
|
|
364
|
+
const isWin = process.platform === 'win32';
|
|
365
|
+
const prevCmd = settings.statusLine?.command || '';
|
|
366
|
+
const needsUpdate = !settings.statusLine
|
|
367
|
+
|| prevCmd === 'bash ~/.claude/statusline-command.sh'
|
|
368
|
+
|| (isWin && prevCmd.includes('bash.exe'))
|
|
369
|
+
|| (isWin && prevCmd.includes('\\\\'));
|
|
370
|
+
if (needsUpdate) {
|
|
359
371
|
let cmd;
|
|
360
372
|
if (isWin) {
|
|
361
|
-
// Windows
|
|
362
|
-
//
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
cmd = `"${gitBash}" "${script}"`;
|
|
373
|
+
// Windows: use Node.js renderer directly — avoids Git Bash/MSYS2 overhead
|
|
374
|
+
// (~50-100ms per subprocess spawn in Git Bash vs single Node.js process)
|
|
375
|
+
const nodeScript = path.join(CLAUDE_DIR, 'statusline-node.js');
|
|
376
|
+
cmd = `node "${nodeScript.replace(/\\/g, '/')}"`;
|
|
366
377
|
} else {
|
|
367
378
|
cmd = 'bash ~/.claude/statusline-command.sh';
|
|
368
379
|
}
|
|
369
380
|
settings.statusLine = { type: 'command', command: cmd };
|
|
370
381
|
writeSettings(settings);
|
|
371
382
|
success(`${B}statusLine${R} config added to settings.json`);
|
|
383
|
+
if (isWin) {
|
|
384
|
+
info(`Using Node.js renderer (fast on Windows)`);
|
|
385
|
+
}
|
|
372
386
|
} else {
|
|
373
387
|
success(`statusLine already configured in settings.json`);
|
|
374
388
|
}
|
|
@@ -408,8 +422,12 @@ async function install() {
|
|
|
408
422
|
}
|
|
409
423
|
|
|
410
424
|
blank();
|
|
411
|
-
|
|
412
|
-
|
|
425
|
+
if (isWin) {
|
|
426
|
+
bar(`Renderer: ${R}${CYN}~/.claude/statusline-node.js${R} ${D}(Node.js)${R}`);
|
|
427
|
+
} else {
|
|
428
|
+
bar(`Script: ${R}${CYN}~/.claude/statusline-command.sh${R}`);
|
|
429
|
+
bar(`Engine: ${R}${CYN}~/.claude/statusline/core.sh${R}`);
|
|
430
|
+
}
|
|
413
431
|
bar(`Config: ${R}${CYN}~/.claude/statusline-config.json${R}`);
|
|
414
432
|
bar(`Settings: ${R}${CYN}~/.claude/settings.json${R}`);
|
|
415
433
|
blank();
|
|
@@ -431,13 +449,18 @@ function uninstall() {
|
|
|
431
449
|
success(`Removed ~/.claude/statusline/`);
|
|
432
450
|
}
|
|
433
451
|
|
|
434
|
-
// Remove
|
|
452
|
+
// Remove scripts
|
|
435
453
|
if (fs.existsSync(SCRIPT_DEST)) {
|
|
436
454
|
fs.unlinkSync(SCRIPT_DEST);
|
|
437
455
|
success(`Removed ~/.claude/statusline-command.sh`);
|
|
438
456
|
} else {
|
|
439
457
|
warn(`statusline-command.sh not found`);
|
|
440
458
|
}
|
|
459
|
+
const nodeScript = path.join(CLAUDE_DIR, 'statusline-node.js');
|
|
460
|
+
if (fs.existsSync(nodeScript)) {
|
|
461
|
+
fs.unlinkSync(nodeScript);
|
|
462
|
+
success(`Removed ~/.claude/statusline-node.js`);
|
|
463
|
+
}
|
|
441
464
|
|
|
442
465
|
// Remove config
|
|
443
466
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
@@ -524,40 +547,53 @@ function preview() {
|
|
|
524
547
|
agent: { name: 'code-reviewer' }
|
|
525
548
|
});
|
|
526
549
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
550
|
+
const isWin = process.platform === 'win32';
|
|
551
|
+
|
|
552
|
+
// On Windows, prefer Node.js renderer for speed
|
|
553
|
+
const nodeRendererInstalled = path.join(CLAUDE_DIR, 'statusline-node.js');
|
|
554
|
+
const nodeRendererPkg = path.join(PKG_DIR, 'bin', 'statusline-node.js');
|
|
555
|
+
const nodeRenderer = fs.existsSync(nodeRendererInstalled) ? nodeRendererInstalled : nodeRendererPkg;
|
|
556
|
+
|
|
557
|
+
if (isWin || !fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
|
|
558
|
+
// Use Node.js renderer
|
|
559
|
+
try {
|
|
560
|
+
const result = execSync(`node "${nodeRenderer.replace(/\\/g, '/')}"`, {
|
|
561
|
+
input: sampleJson,
|
|
562
|
+
encoding: 'utf8',
|
|
563
|
+
timeout: 5000
|
|
564
|
+
});
|
|
565
|
+
log('');
|
|
566
|
+
log(result);
|
|
567
|
+
log('');
|
|
568
|
+
} catch (e) {
|
|
569
|
+
warn(`Preview failed: ${e.message}`);
|
|
570
|
+
}
|
|
534
571
|
} else {
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
572
|
+
// Unix: use bash engine with theme/layout overrides
|
|
573
|
+
const coreFile = path.join(SL_DIR, 'core.sh');
|
|
574
|
+
const scriptPath = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
|
|
538
575
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
576
|
+
const env = { ...process.env };
|
|
577
|
+
if (themeName) env.STATUSLINE_THEME_OVERRIDE = themeName;
|
|
578
|
+
if (layoutName) env.STATUSLINE_LAYOUT_OVERRIDE = layoutName;
|
|
542
579
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
env.HOME = PKG_DIR;
|
|
547
|
-
}
|
|
580
|
+
if (!fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
|
|
581
|
+
env.HOME = PKG_DIR;
|
|
582
|
+
}
|
|
548
583
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
584
|
+
try {
|
|
585
|
+
const escaped = sampleJson.replace(/'/g, "'\\''");
|
|
586
|
+
const result = execSync(`printf '%s' '${escaped}' | bash "${scriptPath.replace(/\\/g, '/')}"`, {
|
|
587
|
+
encoding: 'utf8',
|
|
588
|
+
env,
|
|
589
|
+
timeout: 5000
|
|
590
|
+
});
|
|
591
|
+
log('');
|
|
592
|
+
log(result);
|
|
593
|
+
log('');
|
|
594
|
+
} catch (e) {
|
|
595
|
+
warn(`Preview failed: ${e.message}`);
|
|
596
|
+
}
|
|
561
597
|
}
|
|
562
598
|
}
|
|
563
599
|
|
|
@@ -816,23 +852,55 @@ function doctor() {
|
|
|
816
852
|
warn(`No config file (using defaults)`);
|
|
817
853
|
}
|
|
818
854
|
|
|
819
|
-
// 9.
|
|
855
|
+
// 9. Node.js renderer
|
|
856
|
+
const nodeRendererPath = path.join(CLAUDE_DIR, 'statusline-node.js');
|
|
857
|
+
if (fs.existsSync(nodeRendererPath)) {
|
|
858
|
+
success(`Node.js renderer installed (statusline-node.js)`);
|
|
859
|
+
} else if (process.platform === 'win32') {
|
|
860
|
+
fail(`Node.js renderer missing — required on Windows`);
|
|
861
|
+
info(`Run ${CYN}ccsl install${R} to fix`);
|
|
862
|
+
issues++;
|
|
863
|
+
} else {
|
|
864
|
+
bar(`Node.js renderer not installed (optional on Unix)`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// 10. Performance benchmark
|
|
820
868
|
blank();
|
|
821
869
|
info(`${B}Performance benchmark${R}`);
|
|
822
870
|
blank();
|
|
823
871
|
try {
|
|
824
872
|
const sampleJson = '{"model":{"id":"claude-opus-4-6","display_name":"Opus"},"workspace":{"current_dir":"/tmp"},"cost":{"total_cost_usd":0.5},"context_window":{"context_window_size":200000,"used_percentage":50,"current_usage":{"input_tokens":90000,"output_tokens":10000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}';
|
|
825
|
-
|
|
826
|
-
|
|
873
|
+
|
|
874
|
+
// Benchmark Node.js renderer
|
|
875
|
+
if (fs.existsSync(nodeRendererPath)) {
|
|
827
876
|
const start = Date.now();
|
|
828
|
-
execSync(`
|
|
877
|
+
execSync(`node "${nodeRendererPath.replace(/\\/g, '/')}"`, {
|
|
878
|
+
input: sampleJson,
|
|
829
879
|
encoding: 'utf8',
|
|
830
880
|
timeout: 10000
|
|
831
881
|
});
|
|
832
882
|
const elapsed = Date.now() - start;
|
|
833
883
|
const color = elapsed < 50 ? GRN : elapsed < 100 ? YLW : RED;
|
|
834
884
|
const label = elapsed < 50 ? 'excellent' : elapsed < 100 ? 'good' : 'slow';
|
|
835
|
-
log(` ${GRAY}\u2502${R} ${color}\u25CF${R}
|
|
885
|
+
log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Node.js renderer: ${color}${B}${elapsed}ms${R} (${label})`);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Benchmark bash engine (skip on Windows — known slow)
|
|
889
|
+
if (process.platform !== 'win32') {
|
|
890
|
+
const target = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
|
|
891
|
+
if (target && fs.existsSync(target)) {
|
|
892
|
+
const start = Date.now();
|
|
893
|
+
execSync(`printf '%s' '${sampleJson}' | bash "${target.replace(/\\/g, '/')}"`, {
|
|
894
|
+
encoding: 'utf8',
|
|
895
|
+
timeout: 10000
|
|
896
|
+
});
|
|
897
|
+
const elapsed = Date.now() - start;
|
|
898
|
+
const color = elapsed < 50 ? GRN : elapsed < 100 ? YLW : RED;
|
|
899
|
+
const label = elapsed < 50 ? 'excellent' : elapsed < 100 ? 'good' : 'slow';
|
|
900
|
+
log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Bash engine: ${color}${B}${elapsed}ms${R} (${label})`);
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
bar(`Bash benchmark skipped (slow on Windows, using Node.js)`);
|
|
836
904
|
}
|
|
837
905
|
} catch (e) {
|
|
838
906
|
fail(`Benchmark failed: ${e.message.substring(0, 50)}`);
|
package/bin/statusline-node.js
CHANGED
|
@@ -1,85 +1,132 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// skill-statusline v2.
|
|
3
|
-
// Zero bash dependency — works on Windows, macOS, Linux
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
'use strict';
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// skill-statusline v2.4 — Node.js statusline renderer
|
|
3
|
+
// Zero bash dependency — works on Windows, macOS, Linux
|
|
4
|
+
// Async stdin with hard 1.5s timeout to prevent hangs
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// Hard kill — prevents hanging if stdin pipe never closes on Windows
|
|
11
|
+
setTimeout(() => process.exit(0), 1500);
|
|
12
|
+
|
|
13
|
+
let input = '';
|
|
14
|
+
process.stdin.setEncoding('utf8');
|
|
15
|
+
process.stdin.on('data', c => input += c);
|
|
16
|
+
process.stdin.on('end', () => {
|
|
17
|
+
try { if (input) render(JSON.parse(input)); } catch (e) {}
|
|
18
|
+
process.exit(0);
|
|
19
|
+
});
|
|
20
|
+
process.stdin.on('error', () => process.exit(0));
|
|
21
|
+
process.stdin.resume();
|
|
22
|
+
|
|
23
|
+
function getActivity(transcriptPath) {
|
|
24
|
+
if (!transcriptPath) return 'Idle';
|
|
25
|
+
try {
|
|
26
|
+
const stat = fs.statSync(transcriptPath);
|
|
27
|
+
const readSize = Math.min(16384, stat.size);
|
|
28
|
+
const buf = Buffer.alloc(readSize);
|
|
29
|
+
const fd = fs.openSync(transcriptPath, 'r');
|
|
30
|
+
fs.readSync(fd, buf, 0, readSize, Math.max(0, stat.size - readSize));
|
|
31
|
+
fs.closeSync(fd);
|
|
32
|
+
const lines = buf.toString('utf8').split('\n').filter(l => l.trim());
|
|
33
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
34
|
+
try {
|
|
35
|
+
const entry = JSON.parse(lines[i]);
|
|
36
|
+
if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
37
|
+
const toolUses = entry.message.content.filter(c => c.type === 'tool_use');
|
|
38
|
+
if (toolUses.length) {
|
|
39
|
+
const last = toolUses[toolUses.length - 1];
|
|
40
|
+
const name = last.name;
|
|
41
|
+
const inp = last.input || {};
|
|
42
|
+
if (name === 'Task' && inp.subagent_type) {
|
|
43
|
+
const desc = inp.description ? ': ' + inp.description.slice(0, 25) : '';
|
|
44
|
+
return `Task(${inp.subagent_type}${desc})`;
|
|
45
|
+
}
|
|
46
|
+
if (name === 'Skill' && inp.skill) return `Skill(${inp.skill})`;
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (e) { continue; }
|
|
51
|
+
}
|
|
52
|
+
} catch (e) { /* ignore */ }
|
|
53
|
+
return 'Idle';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getGitInfo(projectDir) {
|
|
57
|
+
let branch = '', remote = '';
|
|
58
|
+
try {
|
|
59
|
+
const gitHead = fs.readFileSync(path.join(projectDir, '.git', 'HEAD'), 'utf8').trim();
|
|
60
|
+
branch = gitHead.startsWith('ref: refs/heads/') ? gitHead.slice(16) : gitHead.slice(0, 7);
|
|
61
|
+
} catch (e) { return 'no-git'; }
|
|
62
|
+
try {
|
|
63
|
+
const config = fs.readFileSync(path.join(projectDir, '.git', 'config'), 'utf8');
|
|
64
|
+
const urlMatch = config.match(/\[remote "origin"\][^[]*url\s*=\s*(.+)/);
|
|
65
|
+
if (urlMatch) {
|
|
66
|
+
const url = urlMatch[1].trim();
|
|
67
|
+
const ghMatch = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
68
|
+
if (ghMatch) remote = ghMatch[1] + '/' + ghMatch[2];
|
|
69
|
+
}
|
|
70
|
+
} catch (e) { /* ignore */ }
|
|
71
|
+
return remote ? `${remote}:${branch}` : branch;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function render(data) {
|
|
75
|
+
const RST = '\x1b[0m', BOLD = '\x1b[1m';
|
|
76
|
+
const CYAN = '\x1b[38;2;6;182;212m', PURPLE = '\x1b[38;2;168;85;247m';
|
|
77
|
+
const GREEN = '\x1b[38;2;34;197;94m', YELLOW = '\x1b[38;2;245;158;11m';
|
|
78
|
+
const RED = '\x1b[38;2;239;68;68m', ORANGE = '\x1b[38;2;251;146;60m';
|
|
79
|
+
const WHITE = '\x1b[38;2;228;228;231m';
|
|
80
|
+
const SEP = '\x1b[38;2;55;55;62m', DIM = '\x1b[38;2;40;40;45m';
|
|
81
|
+
const BLUE = '\x1b[38;2;59;130;246m';
|
|
82
|
+
|
|
83
|
+
const model = data.model?.display_name || 'unknown';
|
|
84
|
+
|
|
85
|
+
const cwd = (data.workspace?.current_dir || data.cwd || '').replace(/\\/g, '/').replace(/\/\/+/g, '/');
|
|
86
|
+
const parts = cwd.split('/').filter(Boolean);
|
|
87
|
+
const dir = parts.length > 3 ? parts.slice(-3).join('/') : parts.length > 0 ? parts.join('/') : '~';
|
|
88
|
+
|
|
89
|
+
const projectDir = data.workspace?.project_dir || data.workspace?.current_dir || data.cwd || '';
|
|
90
|
+
const gitInfo = getGitInfo(projectDir);
|
|
91
|
+
|
|
92
|
+
const activity = getActivity(data.transcript_path);
|
|
93
|
+
|
|
94
|
+
let pct = Math.floor(data.context_window?.used_percentage || 0);
|
|
95
|
+
if (pct > 100) pct = 100;
|
|
96
|
+
const ctxClr = pct > 90 ? RED : pct > 75 ? ORANGE : pct > 40 ? YELLOW : WHITE;
|
|
97
|
+
const barW = 40;
|
|
98
|
+
const filled = Math.min(Math.floor(pct * barW / 100), barW);
|
|
99
|
+
const bar = ctxClr + '\u2588'.repeat(filled) + RST + DIM + '\u2591'.repeat(barW - filled) + RST;
|
|
100
|
+
|
|
101
|
+
const costRaw = data.cost?.total_cost_usd || 0;
|
|
102
|
+
const cost = costRaw === 0 ? '$0.00' : costRaw < 0.01 ? `$${costRaw.toFixed(4)}` : `$${costRaw.toFixed(2)}`;
|
|
103
|
+
|
|
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
|
+
const durMs = data.cost?.total_duration_ms || 0;
|
|
112
|
+
const durMin = Math.floor(durMs / 60000);
|
|
113
|
+
const durSec = Math.floor((durMs % 60000) / 1000);
|
|
114
|
+
const duration = durMin > 0 ? `${durMin}m ${durSec}s` : `${durSec}s`;
|
|
115
|
+
|
|
116
|
+
const actClr = activity === 'Idle' ? DIM : GREEN;
|
|
117
|
+
|
|
118
|
+
const S = ` ${SEP}\u2502${RST} `;
|
|
119
|
+
const rpad = (s, w) => {
|
|
120
|
+
const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
121
|
+
return s + (plain.length < w ? ' '.repeat(w - plain.length) : '');
|
|
122
|
+
};
|
|
123
|
+
const C1 = 44;
|
|
124
|
+
|
|
125
|
+
let out = '';
|
|
126
|
+
out += ' ' + rpad(`${actClr}Action:${RST} ${actClr}${activity}${RST}`, C1) + S + `${WHITE}Git:${RST} ${WHITE}${gitInfo}${RST}\n`;
|
|
127
|
+
out += ' ' + rpad(`${PURPLE}Model:${RST} ${PURPLE}${BOLD}${model}${RST}`, C1) + S + `${CYAN}Dir:${RST} ${CYAN}${dir}${RST}\n`;
|
|
128
|
+
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`;
|
|
129
|
+
out += ' ' + rpad(`${BLUE}Session:${RST} ${BLUE}${duration}${RST}`, C1) + S + `${ctxClr}Context:${RST} ${bar} ${ctxClr}${pct}%${RST}`;
|
|
130
|
+
|
|
131
|
+
process.stdout.write(out);
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-statusline",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display.
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display. Node.js renderer on Windows (no bash overhead), pure bash on Unix.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skill-statusline": "bin/cli.js",
|
|
7
7
|
"ccsl": "bin/cli.js"
|