@chongdashu/cc-statusline 1.2.7 → 1.3.2
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/CHANGELOG.md +157 -110
- package/CLAUDE.md +76 -61
- package/CONTRIBUTING.md +207 -207
- package/LICENSE +20 -20
- package/README.md +373 -305
- package/dist/index.js +206 -22
- package/dist/index.js.map +1 -1
- package/package.json +57 -57
- package/test/test-concurrent-locking.sh +54 -54
- package/test/test-installation.sh +335 -335
- package/test/test-statusline-with-lock.sh +67 -67
package/dist/index.js
CHANGED
|
@@ -441,15 +441,38 @@ burn_color() { :; }
|
|
|
441
441
|
session_color() { :; }
|
|
442
442
|
`;
|
|
443
443
|
return `${colorCode}
|
|
444
|
-
# ----
|
|
444
|
+
# ---- cost and usage extraction ----
|
|
445
445
|
session_txt=""; session_pct=0; session_bar=""
|
|
446
446
|
cost_usd=""; cost_per_hour=""; tpm=""; tot_tokens=""
|
|
447
447
|
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
# Extract cost data from Claude Code input
|
|
449
|
+
if [ "$HAS_JQ" -eq 1 ]; then
|
|
450
|
+
# Get cost data from Claude Code's input
|
|
451
|
+
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty' 2>/dev/null)
|
|
452
|
+
total_duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // empty' 2>/dev/null)
|
|
453
|
+
|
|
454
|
+
# Calculate burn rate ($/hour) from cost and duration
|
|
455
|
+
if [ -n "$cost_usd" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then
|
|
456
|
+
# Convert ms to hours and calculate rate
|
|
457
|
+
cost_per_hour=$(echo "$cost_usd $total_duration_ms" | awk '{printf "%.2f", $1 * 3600000 / $2}')
|
|
458
|
+
fi
|
|
459
|
+
else
|
|
460
|
+
# Bash fallback for cost extraction
|
|
461
|
+
cost_usd=$(echo "$input" | grep -o '"total_cost_usd"[[:space:]]*:[[:space:]]*[0-9.]*' | sed 's/.*:[[:space:]]*\\([0-9.]*\\).*/\\1/')
|
|
462
|
+
total_duration_ms=$(echo "$input" | grep -o '"total_duration_ms"[[:space:]]*:[[:space:]]*[0-9]*' | sed 's/.*:[[:space:]]*\\([0-9]*\\).*/\\1/')
|
|
463
|
+
|
|
464
|
+
# Calculate burn rate ($/hour) from cost and duration
|
|
465
|
+
if [ -n "$cost_usd" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then
|
|
466
|
+
# Convert ms to hours and calculate rate
|
|
467
|
+
cost_per_hour=$(echo "$cost_usd $total_duration_ms" | awk '{printf "%.2f", $1 * 3600000 / $2}')
|
|
468
|
+
fi
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
# Get token data and session info from ccusage if available
|
|
472
|
+
if command -v ccusage >/dev/null 2>&1 && [ "$HAS_JQ" -eq 1 ]; then
|
|
450
473
|
blocks_output=""
|
|
451
474
|
|
|
452
|
-
# Try ccusage with timeout
|
|
475
|
+
# Try ccusage with timeout for token data and session info
|
|
453
476
|
if command -v timeout >/dev/null 2>&1; then
|
|
454
477
|
blocks_output=$(timeout 5s ccusage blocks --json 2>/dev/null)
|
|
455
478
|
elif command -v gtimeout >/dev/null 2>&1; then
|
|
@@ -461,13 +484,13 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
461
484
|
fi
|
|
462
485
|
if [ -n "$blocks_output" ]; then
|
|
463
486
|
active_block=$(echo "$blocks_output" | jq -c '.blocks[] | select(.isActive == true)' 2>/dev/null | head -n1)
|
|
464
|
-
if [ -n "$active_block" ]; then${config.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
487
|
+
if [ -n "$active_block" ]; then${config.showTokens ? `
|
|
488
|
+
# Get token count from ccusage
|
|
489
|
+
tot_tokens=$(echo "$active_block" | jq -r '.totalTokens // empty')` : ""}${config.showBurnRate && config.showTokens ? `
|
|
490
|
+
# Get tokens per minute from ccusage
|
|
468
491
|
tpm=$(echo "$active_block" | jq -r '.burnRate.tokensPerMinute // empty')` : ""}${config.showSession || config.showProgressBar ? `
|
|
469
492
|
|
|
470
|
-
# Session time calculation
|
|
493
|
+
# Session time calculation from ccusage
|
|
471
494
|
reset_time_str=$(echo "$active_block" | jq -r '.usageLimitResetTime // .endTime // empty')
|
|
472
495
|
start_time_str=$(echo "$active_block" | jq -r '.startTime // empty')
|
|
473
496
|
|
|
@@ -515,6 +538,7 @@ progress_bar() {
|
|
|
515
538
|
}
|
|
516
539
|
|
|
517
540
|
// src/generators/bash-generator.ts
|
|
541
|
+
var VERSION = "1.3.2";
|
|
518
542
|
function generateBashStatusline(config) {
|
|
519
543
|
const hasGit = config.features.includes("git");
|
|
520
544
|
const hasUsage = config.features.some((f) => ["usage", "session", "tokens", "burnrate"].includes(f));
|
|
@@ -538,9 +562,10 @@ function generateBashStatusline(config) {
|
|
|
538
562
|
};
|
|
539
563
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
540
564
|
const script = `#!/bin/bash
|
|
541
|
-
# Generated by cc-statusline (https://www.npmjs.com/package/@chongdashu/cc-statusline)
|
|
565
|
+
# Generated by cc-statusline v${VERSION} (https://www.npmjs.com/package/@chongdashu/cc-statusline)
|
|
542
566
|
# Custom Claude Code statusline - Created: ${timestamp}
|
|
543
567
|
# Theme: ${config.theme} | Colors: ${config.colors} | Features: ${config.features.join(", ")}
|
|
568
|
+
STATUSLINE_VERSION="${VERSION}"
|
|
544
569
|
|
|
545
570
|
input=$(cat)
|
|
546
571
|
${config.logging ? generateLoggingCode() : ""}
|
|
@@ -559,21 +584,72 @@ ${generateDisplaySection(config, gitConfig, usageConfig)}
|
|
|
559
584
|
}
|
|
560
585
|
function generateLoggingCode() {
|
|
561
586
|
return `
|
|
562
|
-
|
|
587
|
+
# Get the directory where this statusline script is located
|
|
588
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
589
|
+
LOG_FILE="\${SCRIPT_DIR}/statusline.log"
|
|
563
590
|
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
|
564
591
|
|
|
592
|
+
# ---- check jq availability ----
|
|
593
|
+
HAS_JQ=0
|
|
594
|
+
if command -v jq >/dev/null 2>&1; then
|
|
595
|
+
HAS_JQ=1
|
|
596
|
+
fi
|
|
597
|
+
|
|
565
598
|
# ---- logging ----
|
|
566
599
|
{
|
|
567
|
-
echo "[$TIMESTAMP] Status line triggered
|
|
568
|
-
|
|
600
|
+
echo "[$TIMESTAMP] Status line triggered (cc-statusline v\${STATUSLINE_VERSION})"
|
|
601
|
+
echo "[$TIMESTAMP] Input:"
|
|
602
|
+
if [ "$HAS_JQ" -eq 1 ]; then
|
|
603
|
+
echo "$input" | jq . 2>/dev/null || echo "$input"
|
|
604
|
+
echo "[$TIMESTAMP] Using jq for JSON parsing"
|
|
605
|
+
else
|
|
606
|
+
echo "$input"
|
|
607
|
+
echo "[$TIMESTAMP] WARNING: jq not found, using bash fallback for JSON parsing"
|
|
608
|
+
fi
|
|
569
609
|
echo "---"
|
|
570
610
|
} >> "$LOG_FILE" 2>/dev/null
|
|
571
611
|
`;
|
|
572
612
|
}
|
|
613
|
+
function generateJsonExtractorCode() {
|
|
614
|
+
return `
|
|
615
|
+
# ---- JSON extraction utilities ----
|
|
616
|
+
# Pure bash JSON value extractor (fallback when jq not available)
|
|
617
|
+
extract_json_string() {
|
|
618
|
+
local json="$1"
|
|
619
|
+
local key="$2"
|
|
620
|
+
local default="\${3:-}"
|
|
621
|
+
|
|
622
|
+
# For nested keys like workspace.current_dir, get the last part
|
|
623
|
+
local field="\${key##*.}"
|
|
624
|
+
field="\${field%% *}" # Remove any jq operators
|
|
625
|
+
|
|
626
|
+
# Try to extract string value (quoted)
|
|
627
|
+
local value=$(echo "$json" | grep -o "\\"\\\${field}\\"[[:space:]]*:[[:space:]]*\\"[^\\"]*\\"" | head -1 | sed 's/.*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
|
|
628
|
+
|
|
629
|
+
# Convert escaped backslashes to forward slashes for Windows paths
|
|
630
|
+
if [ -n "$value" ]; then
|
|
631
|
+
value=$(echo "$value" | sed 's/\\\\\\\\/\\//g')
|
|
632
|
+
fi
|
|
633
|
+
|
|
634
|
+
# If no string value found, try to extract number value (unquoted)
|
|
635
|
+
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
|
636
|
+
value=$(echo "$json" | grep -o "\\"\\\${field}\\"[[:space:]]*:[[:space:]]*[0-9.]\\+" | head -1 | sed 's/.*:[[:space:]]*\\([0-9.]\\+\\).*/\\1/')
|
|
637
|
+
fi
|
|
638
|
+
|
|
639
|
+
# Return value or default
|
|
640
|
+
if [ -n "$value" ] && [ "$value" != "null" ]; then
|
|
641
|
+
echo "$value"
|
|
642
|
+
else
|
|
643
|
+
echo "$default"
|
|
644
|
+
fi
|
|
645
|
+
}
|
|
646
|
+
`;
|
|
647
|
+
}
|
|
573
648
|
function generateBasicDataExtraction(hasDirectory, hasModel, hasContext) {
|
|
574
649
|
return `
|
|
650
|
+
${generateJsonExtractorCode()}
|
|
575
651
|
# ---- basics ----
|
|
576
|
-
if
|
|
652
|
+
if [ "$HAS_JQ" -eq 1 ]; then${hasDirectory ? `
|
|
577
653
|
current_dir=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "unknown"' 2>/dev/null | sed "s|^$HOME|~|g")` : ""}${hasModel ? `
|
|
578
654
|
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null)
|
|
579
655
|
model_version=$(echo "$input" | jq -r '.model.version // ""' 2>/dev/null)` : ""}${hasContext ? `
|
|
@@ -581,11 +657,29 @@ if command -v jq >/dev/null 2>&1; then${hasDirectory ? `
|
|
|
581
657
|
cc_version=$(echo "$input" | jq -r '.version // ""' 2>/dev/null)
|
|
582
658
|
output_style=$(echo "$input" | jq -r '.output_style.name // ""' 2>/dev/null)
|
|
583
659
|
else${hasDirectory ? `
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
660
|
+
# Bash fallback for JSON extraction
|
|
661
|
+
# Extract current_dir from workspace object - look for the pattern workspace":{"current_dir":"..."}
|
|
662
|
+
current_dir=$(echo "$input" | grep -o '"workspace"[[:space:]]*:[[:space:]]*{[^}]*"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/' | sed 's/\\\\\\\\/\\//g')
|
|
663
|
+
|
|
664
|
+
# Fall back to cwd if workspace extraction failed
|
|
665
|
+
if [ -z "$current_dir" ] || [ "$current_dir" = "null" ]; then
|
|
666
|
+
current_dir=$(echo "$input" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/' | sed 's/\\\\\\\\/\\//g')
|
|
667
|
+
fi
|
|
668
|
+
|
|
669
|
+
# Fallback to unknown if all extraction failed
|
|
670
|
+
[ -z "$current_dir" ] && current_dir="unknown"
|
|
671
|
+
current_dir=$(echo "$current_dir" | sed "s|^$HOME|~|g")` : ""}${hasModel ? `
|
|
672
|
+
|
|
673
|
+
# Extract model name from nested model object
|
|
674
|
+
model_name=$(echo "$input" | grep -o '"model"[[:space:]]*:[[:space:]]*{[^}]*"display_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"display_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
|
|
675
|
+
[ -z "$model_name" ] && model_name="Claude"
|
|
676
|
+
# Model version is in the model ID, not a separate field
|
|
677
|
+
model_version="" # Not available in Claude Code JSON` : ""}${hasContext ? `
|
|
678
|
+
session_id=$(extract_json_string "$input" "session_id" "")` : ""}
|
|
679
|
+
# CC version is at the root level
|
|
680
|
+
cc_version=$(echo "$input" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
|
|
681
|
+
# Output style is nested
|
|
682
|
+
output_style=$(echo "$input" | grep -o '"output_style"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
|
|
589
683
|
fi
|
|
590
684
|
`;
|
|
591
685
|
}
|
|
@@ -617,7 +711,7 @@ get_max_context() {
|
|
|
617
711
|
esac
|
|
618
712
|
}
|
|
619
713
|
|
|
620
|
-
if [ -n "$session_id" ] &&
|
|
714
|
+
if [ -n "$session_id" ] && [ "$HAS_JQ" -eq 1 ]; then
|
|
621
715
|
MAX_CONTEXT=$(get_max_context "$model_name")
|
|
622
716
|
|
|
623
717
|
# Convert current dir to session file path
|
|
@@ -652,6 +746,9 @@ function generateLoggingOutput() {
|
|
|
652
746
|
# ---- log extracted data ----
|
|
653
747
|
{
|
|
654
748
|
echo "[$TIMESTAMP] Extracted: dir=\${current_dir:-}, model=\${model_name:-}, version=\${model_version:-}, git=\${git_branch:-}, context=\${context_pct:-}, cost=\${cost_usd:-}, cost_ph=\${cost_per_hour:-}, tokens=\${tot_tokens:-}, tpm=\${tpm:-}, session_pct=\${session_pct:-}"
|
|
749
|
+
if [ "$HAS_JQ" -eq 0 ]; then
|
|
750
|
+
echo "[$TIMESTAMP] Note: Context, tokens, and session info require jq for full functionality"
|
|
751
|
+
fi
|
|
655
752
|
} >> "$LOG_FILE" 2>/dev/null
|
|
656
753
|
`;
|
|
657
754
|
}
|
|
@@ -829,7 +926,7 @@ async function updateSettingsJson(claudeDir, scriptName, isGlobal) {
|
|
|
829
926
|
}
|
|
830
927
|
}
|
|
831
928
|
}
|
|
832
|
-
const commandPath = isGlobal ? `~/.claude/${scriptName}` : `.claude/${scriptName}`;
|
|
929
|
+
const commandPath = process.platform === "win32" ? `bash ${isGlobal ? ".claude" : ".claude"}/${scriptName}` : isGlobal ? `~/.claude/${scriptName}` : `.claude/${scriptName}`;
|
|
833
930
|
settings.statusLine = {
|
|
834
931
|
type: "command",
|
|
835
932
|
command: commandPath,
|
|
@@ -847,11 +944,97 @@ import chalk from "chalk";
|
|
|
847
944
|
import ora from "ora";
|
|
848
945
|
import path4 from "path";
|
|
849
946
|
import os2 from "os";
|
|
947
|
+
import { execSync } from "child_process";
|
|
948
|
+
function checkJqInstallation() {
|
|
949
|
+
try {
|
|
950
|
+
execSync("command -v jq", { stdio: "ignore" });
|
|
951
|
+
return true;
|
|
952
|
+
} catch {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function getJqInstallInstructions() {
|
|
957
|
+
const platform = process.platform;
|
|
958
|
+
if (platform === "darwin") {
|
|
959
|
+
return `
|
|
960
|
+
${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")}
|
|
961
|
+
|
|
962
|
+
${chalk.green("Using Homebrew (recommended):")}
|
|
963
|
+
brew install jq
|
|
964
|
+
|
|
965
|
+
${chalk.green("Using MacPorts:")}
|
|
966
|
+
sudo port install jq
|
|
967
|
+
|
|
968
|
+
${chalk.green("Or download directly:")}
|
|
969
|
+
https://github.com/jqlang/jq/releases
|
|
970
|
+
`;
|
|
971
|
+
} else if (platform === "linux") {
|
|
972
|
+
return `
|
|
973
|
+
${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")}
|
|
974
|
+
|
|
975
|
+
${chalk.green("Ubuntu/Debian:")}
|
|
976
|
+
sudo apt-get install jq
|
|
977
|
+
|
|
978
|
+
${chalk.green("CentOS/RHEL/Fedora:")}
|
|
979
|
+
sudo yum install jq
|
|
980
|
+
|
|
981
|
+
${chalk.green("Arch Linux:")}
|
|
982
|
+
sudo pacman -S jq
|
|
983
|
+
|
|
984
|
+
${chalk.green("Or download directly:")}
|
|
985
|
+
https://github.com/jqlang/jq/releases
|
|
986
|
+
`;
|
|
987
|
+
} else if (platform === "win32") {
|
|
988
|
+
return `
|
|
989
|
+
${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")}
|
|
990
|
+
|
|
991
|
+
${chalk.green("Option 1: Using Package Manager")}
|
|
992
|
+
${chalk.dim("Chocolatey:")} choco install jq
|
|
993
|
+
${chalk.dim("Scoop:")} scoop install jq
|
|
994
|
+
|
|
995
|
+
${chalk.green("Option 2: Manual Download")}
|
|
996
|
+
1. Download from: https://github.com/jqlang/jq/releases/latest
|
|
997
|
+
2. Choose file:
|
|
998
|
+
${chalk.dim("\u2022 64-bit Windows:")} jq-windows-amd64.exe
|
|
999
|
+
${chalk.dim("\u2022 32-bit Windows:")} jq-windows-i386.exe
|
|
1000
|
+
3. Rename to: jq.exe
|
|
1001
|
+
4. Move to: C:\\Windows\\System32\\ ${chalk.dim("(or add to PATH)")}
|
|
1002
|
+
5. Test: Open new terminal and run: jq --version
|
|
1003
|
+
`;
|
|
1004
|
+
} else {
|
|
1005
|
+
return `
|
|
1006
|
+
${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")}
|
|
1007
|
+
|
|
1008
|
+
${chalk.green("Download for your platform:")}
|
|
1009
|
+
https://github.com/jqlang/jq/releases
|
|
1010
|
+
`;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
850
1013
|
async function initCommand(options) {
|
|
851
1014
|
try {
|
|
852
1015
|
const spinner = ora("Initializing statusline generator...").start();
|
|
853
1016
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
854
1017
|
spinner.stop();
|
|
1018
|
+
const hasJq = checkJqInstallation();
|
|
1019
|
+
if (!hasJq) {
|
|
1020
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F jq is not installed"));
|
|
1021
|
+
console.log(chalk.dim("Your statusline will work without jq, but with limited functionality:"));
|
|
1022
|
+
console.log(chalk.dim(" \u2022 Context remaining percentage won't be displayed"));
|
|
1023
|
+
console.log(chalk.dim(" \u2022 Token statistics may not work"));
|
|
1024
|
+
console.log(chalk.dim(" \u2022 Performance will be slower"));
|
|
1025
|
+
console.log(getJqInstallInstructions());
|
|
1026
|
+
const inquirer3 = (await import("inquirer")).default;
|
|
1027
|
+
const { continueWithoutJq } = await inquirer3.prompt([{
|
|
1028
|
+
type: "confirm",
|
|
1029
|
+
name: "continueWithoutJq",
|
|
1030
|
+
message: "Continue without jq?",
|
|
1031
|
+
default: true
|
|
1032
|
+
}]);
|
|
1033
|
+
if (!continueWithoutJq) {
|
|
1034
|
+
console.log(chalk.cyan("\n\u{1F44D} Install jq and run this command again"));
|
|
1035
|
+
process.exit(0);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
855
1038
|
const config = await collectConfiguration();
|
|
856
1039
|
const validation = validateConfig(config);
|
|
857
1040
|
if (!validation.isValid) {
|
|
@@ -929,8 +1112,9 @@ async function initCommand(options) {
|
|
|
929
1112
|
|
|
930
1113
|
// src/index.ts
|
|
931
1114
|
import chalk3 from "chalk";
|
|
1115
|
+
var VERSION2 = "1.3.2";
|
|
932
1116
|
var program = new Command();
|
|
933
|
-
program.name("cc-statusline").description("Interactive CLI tool for generating custom Claude Code statuslines").version(
|
|
1117
|
+
program.name("cc-statusline").description("Interactive CLI tool for generating custom Claude Code statuslines").version(VERSION2);
|
|
934
1118
|
program.command("init").description("Create a custom statusline with interactive prompts").option("-o, --output <path>", "Output path for statusline.sh", "./.claude/statusline.sh").option("--no-install", "Don't automatically install to .claude/statusline.sh").action(initCommand);
|
|
935
1119
|
program.command("preview").description("Preview existing statusline.sh with mock data").argument("<script-path>", "Path to statusline.sh file to preview").action(async (scriptPath) => {
|
|
936
1120
|
const { previewCommand: previewCommand2 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
|