@chongdashu/cc-statusline 1.3.0 → 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 -127
- package/CLAUDE.md +76 -61
- package/CONTRIBUTING.md +207 -207
- package/LICENSE +20 -20
- package/README.md +373 -310
- package/dist/index.js +181 -14
- 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
|
@@ -446,11 +446,21 @@ session_txt=""; session_pct=0; session_bar=""
|
|
|
446
446
|
cost_usd=""; cost_per_hour=""; tpm=""; tot_tokens=""
|
|
447
447
|
|
|
448
448
|
# Extract cost data from Claude Code input
|
|
449
|
-
if
|
|
449
|
+
if [ "$HAS_JQ" -eq 1 ]; then
|
|
450
450
|
# Get cost data from Claude Code's input
|
|
451
451
|
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty' 2>/dev/null)
|
|
452
452
|
total_duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // empty' 2>/dev/null)
|
|
453
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
|
+
|
|
454
464
|
# Calculate burn rate ($/hour) from cost and duration
|
|
455
465
|
if [ -n "$cost_usd" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then
|
|
456
466
|
# Convert ms to hours and calculate rate
|
|
@@ -459,7 +469,7 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
459
469
|
fi
|
|
460
470
|
|
|
461
471
|
# Get token data and session info from ccusage if available
|
|
462
|
-
if command -v ccusage >/dev/null 2>&1 &&
|
|
472
|
+
if command -v ccusage >/dev/null 2>&1 && [ "$HAS_JQ" -eq 1 ]; then
|
|
463
473
|
blocks_output=""
|
|
464
474
|
|
|
465
475
|
# Try ccusage with timeout for token data and session info
|
|
@@ -528,7 +538,7 @@ progress_bar() {
|
|
|
528
538
|
}
|
|
529
539
|
|
|
530
540
|
// src/generators/bash-generator.ts
|
|
531
|
-
var VERSION = "1.3.
|
|
541
|
+
var VERSION = "1.3.2";
|
|
532
542
|
function generateBashStatusline(config) {
|
|
533
543
|
const hasGit = config.features.includes("git");
|
|
534
544
|
const hasUsage = config.features.some((f) => ["usage", "session", "tokens", "burnrate"].includes(f));
|
|
@@ -555,6 +565,7 @@ function generateBashStatusline(config) {
|
|
|
555
565
|
# Generated by cc-statusline v${VERSION} (https://www.npmjs.com/package/@chongdashu/cc-statusline)
|
|
556
566
|
# Custom Claude Code statusline - Created: ${timestamp}
|
|
557
567
|
# Theme: ${config.theme} | Colors: ${config.colors} | Features: ${config.features.join(", ")}
|
|
568
|
+
STATUSLINE_VERSION="${VERSION}"
|
|
558
569
|
|
|
559
570
|
input=$(cat)
|
|
560
571
|
${config.logging ? generateLoggingCode() : ""}
|
|
@@ -578,18 +589,67 @@ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
|
578
589
|
LOG_FILE="\${SCRIPT_DIR}/statusline.log"
|
|
579
590
|
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
|
580
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
|
+
|
|
581
598
|
# ---- logging ----
|
|
582
599
|
{
|
|
583
|
-
echo "[$TIMESTAMP] Status line triggered
|
|
584
|
-
|
|
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
|
|
585
609
|
echo "---"
|
|
586
610
|
} >> "$LOG_FILE" 2>/dev/null
|
|
587
611
|
`;
|
|
588
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
|
+
}
|
|
589
648
|
function generateBasicDataExtraction(hasDirectory, hasModel, hasContext) {
|
|
590
649
|
return `
|
|
650
|
+
${generateJsonExtractorCode()}
|
|
591
651
|
# ---- basics ----
|
|
592
|
-
if
|
|
652
|
+
if [ "$HAS_JQ" -eq 1 ]; then${hasDirectory ? `
|
|
593
653
|
current_dir=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "unknown"' 2>/dev/null | sed "s|^$HOME|~|g")` : ""}${hasModel ? `
|
|
594
654
|
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null)
|
|
595
655
|
model_version=$(echo "$input" | jq -r '.model.version // ""' 2>/dev/null)` : ""}${hasContext ? `
|
|
@@ -597,11 +657,29 @@ if command -v jq >/dev/null 2>&1; then${hasDirectory ? `
|
|
|
597
657
|
cc_version=$(echo "$input" | jq -r '.version // ""' 2>/dev/null)
|
|
598
658
|
output_style=$(echo "$input" | jq -r '.output_style.name // ""' 2>/dev/null)
|
|
599
659
|
else${hasDirectory ? `
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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/')
|
|
605
683
|
fi
|
|
606
684
|
`;
|
|
607
685
|
}
|
|
@@ -633,7 +711,7 @@ get_max_context() {
|
|
|
633
711
|
esac
|
|
634
712
|
}
|
|
635
713
|
|
|
636
|
-
if [ -n "$session_id" ] &&
|
|
714
|
+
if [ -n "$session_id" ] && [ "$HAS_JQ" -eq 1 ]; then
|
|
637
715
|
MAX_CONTEXT=$(get_max_context "$model_name")
|
|
638
716
|
|
|
639
717
|
# Convert current dir to session file path
|
|
@@ -668,6 +746,9 @@ function generateLoggingOutput() {
|
|
|
668
746
|
# ---- log extracted data ----
|
|
669
747
|
{
|
|
670
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
|
|
671
752
|
} >> "$LOG_FILE" 2>/dev/null
|
|
672
753
|
`;
|
|
673
754
|
}
|
|
@@ -845,7 +926,7 @@ async function updateSettingsJson(claudeDir, scriptName, isGlobal) {
|
|
|
845
926
|
}
|
|
846
927
|
}
|
|
847
928
|
}
|
|
848
|
-
const commandPath = isGlobal ? `~/.claude/${scriptName}` : `.claude/${scriptName}`;
|
|
929
|
+
const commandPath = process.platform === "win32" ? `bash ${isGlobal ? ".claude" : ".claude"}/${scriptName}` : isGlobal ? `~/.claude/${scriptName}` : `.claude/${scriptName}`;
|
|
849
930
|
settings.statusLine = {
|
|
850
931
|
type: "command",
|
|
851
932
|
command: commandPath,
|
|
@@ -863,11 +944,97 @@ import chalk from "chalk";
|
|
|
863
944
|
import ora from "ora";
|
|
864
945
|
import path4 from "path";
|
|
865
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
|
+
}
|
|
866
1013
|
async function initCommand(options) {
|
|
867
1014
|
try {
|
|
868
1015
|
const spinner = ora("Initializing statusline generator...").start();
|
|
869
1016
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
870
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
|
+
}
|
|
871
1038
|
const config = await collectConfiguration();
|
|
872
1039
|
const validation = validateConfig(config);
|
|
873
1040
|
if (!validation.isValid) {
|
|
@@ -945,7 +1112,7 @@ async function initCommand(options) {
|
|
|
945
1112
|
|
|
946
1113
|
// src/index.ts
|
|
947
1114
|
import chalk3 from "chalk";
|
|
948
|
-
var VERSION2 = "1.3.
|
|
1115
|
+
var VERSION2 = "1.3.2";
|
|
949
1116
|
var program = new Command();
|
|
950
1117
|
program.name("cc-statusline").description("Interactive CLI tool for generating custom Claude Code statuslines").version(VERSION2);
|
|
951
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);
|