@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/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 command -v jq >/dev/null 2>&1; then
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 && command -v jq >/dev/null 2>&1; then
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.0";
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 with input:"
584
- (echo "$input" | jq . 2>/dev/null) || echo "$input"
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 command -v jq >/dev/null 2>&1; then${hasDirectory ? `
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
- current_dir="unknown"` : ""}${hasModel ? `
601
- model_name="Claude"; model_version=""` : ""}${hasContext ? `
602
- session_id=""` : ""}
603
- cc_version=""
604
- output_style=""
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" ] && command -v jq >/dev/null 2>&1; then
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.0";
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);