@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/dist/index.js CHANGED
@@ -441,15 +441,38 @@ burn_color() { :; }
441
441
  session_color() { :; }
442
442
  `;
443
443
  return `${colorCode}
444
- # ---- ccusage integration ----
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
- if command -v jq >/dev/null 2>&1; then
449
- # Run ccusage directly (it has its own internal caching)
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.showCost ? `
465
- cost_usd=$(echo "$active_block" | jq -r '.costUSD // empty')
466
- cost_per_hour=$(echo "$active_block" | jq -r '.burnRate.costPerHour // empty')` : ""}${config.showTokens ? `
467
- tot_tokens=$(echo "$active_block" | jq -r '.totalTokens // empty')` : ""}${config.showBurnRate ? `
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
- LOG_FILE="\${HOME}/.claude/statusline.log"
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 with input:"
568
- (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
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 command -v jq >/dev/null 2>&1; then${hasDirectory ? `
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
- current_dir="unknown"` : ""}${hasModel ? `
585
- model_name="Claude"; model_version=""` : ""}${hasContext ? `
586
- session_id=""` : ""}
587
- cc_version=""
588
- 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/')
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" ] && command -v jq >/dev/null 2>&1; then
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("1.2.6");
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));