UNPKG

@chongdashu/cc-statusline

Version:

Interactive CLI tool for generating custom Claude Code statuslines

1,159 lines (1,104 loc) 45.6 kB
#!/usr/bin/env node var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // node_modules/tsup/assets/esm_shims.js import path from "path"; import { fileURLToPath } from "url"; var init_esm_shims = __esm({ "node_modules/tsup/assets/esm_shims.js"() { "use strict"; } }); // src/utils/tester.ts var tester_exports = {}; __export(tester_exports, { analyzeTestResult: () => analyzeTestResult, generateMockCcusageOutput: () => generateMockCcusageOutput, generateMockClaudeInput: () => generateMockClaudeInput, testStatuslineScript: () => testStatuslineScript }); import { spawn } from "child_process"; import { promises as fs2 } from "fs"; import path3 from "path"; async function testStatuslineScript(script, mockData) { const startTime = Date.now(); try { const tempDir = "/tmp"; const scriptPath = path3.join(tempDir, `statusline-test-${Date.now()}.sh`); await fs2.writeFile(scriptPath, script, { mode: 493 }); const input = mockData || generateMockClaudeInput(); const result = await executeScript(scriptPath, JSON.stringify(input)); await fs2.unlink(scriptPath).catch(() => { }); const executionTime = Date.now() - startTime; return { success: result.success, output: result.output, error: result.error, executionTime }; } catch (error) { return { success: false, output: "", error: error instanceof Error ? error.message : String(error), executionTime: Date.now() - startTime }; } } function generateMockClaudeInput(config) { return { session_id: "test-session-123", transcript_path: "/home/user/.claude/conversations/test.jsonl", cwd: "/home/user/projects/my-project", workspace: { current_dir: "/home/user/projects/my-project", project_dir: "/home/user/projects/my-project" }, model: { id: "claude-opus-4-1-20250805", display_name: "Opus 4.1", version: "20250805" }, version: "1.0.80", output_style: { name: "default" }, cost: { total_cost_usd: 0.42, total_duration_ms: 18e4, // 3 minutes total_api_duration_ms: 2300, total_lines_added: 156, total_lines_removed: 23 }, context_window: { total_input_tokens: 15234, total_output_tokens: 4521, context_window_size: 2e5, current_usage: { input_tokens: 8500, output_tokens: 1200, cache_creation_input_tokens: 5e3, cache_read_input_tokens: 2e3 } } }; } function generateMockCcusageOutput() { return { blocks: [ { id: "2025-08-13T08:00:00.000Z", startTime: "2025-08-13T08:00:00.000Z", endTime: "2025-08-13T13:00:00.000Z", usageLimitResetTime: "2025-08-13T13:00:00.000Z", actualEndTime: "2025-08-13T09:30:34.698Z", isActive: true, isGap: false, entries: 12, tokenCounts: { inputTokens: 1250, outputTokens: 2830, cacheCreationInputTokens: 15e3, cacheReadInputTokens: 45e3 }, totalTokens: 64080, costUSD: 3.42, models: ["claude-opus-4-1-20250805"], burnRate: { tokensPerMinute: 850.5, tokensPerMinuteForIndicator: 850, costPerHour: 12.45 }, projection: { totalTokens: 128e3, totalCost: 6.84, remainingMinutes: 210 } } ] }; } async function executeScript(scriptPath, input) { return new Promise((resolve) => { const process2 = spawn("bash", [scriptPath], { stdio: ["pipe", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; process2.stdout.on("data", (data) => { stdout += data.toString(); }); process2.stderr.on("data", (data) => { stderr += data.toString(); }); process2.on("close", (code) => { resolve({ success: code === 0, output: stdout.trim(), error: stderr.trim() || void 0 }); }); process2.on("error", (err) => { resolve({ success: false, output: "", error: err.message }); }); process2.stdin.write(input); process2.stdin.end(); setTimeout(() => { process2.kill(); resolve({ success: false, output: stdout, error: "Script execution timed out (5s)" }); }, 5e3); }); } function analyzeTestResult(result, config) { const issues = []; const suggestions = []; let performance; if (result.executionTime > 1e3) { performance = "timeout"; issues.push("Script execution is very slow (>1s)"); } else if (result.executionTime > 500) { performance = "slow"; issues.push("Script execution is slow (>500ms)"); } else if (result.executionTime > 100) { performance = "good"; } else { performance = "excellent"; } let hasRequiredFeatures = true; if (config.features.includes("directory") && !result.output.includes("projects")) { hasRequiredFeatures = false; issues.push("Directory feature not working properly"); } if (config.features.includes("model") && !result.output.includes("Opus")) { hasRequiredFeatures = false; issues.push("Model feature not working properly"); } if (config.features.includes("git") && config.ccusageIntegration && !result.output.includes("git")) { suggestions.push("Git integration may require actual git repository"); } if (result.error) { issues.push(`Script errors: ${result.error}`); } if (!result.success) { issues.push("Script failed to execute successfully"); } if (config.features.length > 6) { suggestions.push("Consider reducing number of features for better performance"); } if (config.ccusageIntegration && result.executionTime > 200) { suggestions.push("ccusage integration may slow down statusline - consider caching"); } return { performance, hasRequiredFeatures, issues, suggestions }; } var init_tester = __esm({ "src/utils/tester.ts"() { "use strict"; init_esm_shims(); } }); // src/cli/preview.ts var preview_exports = {}; __export(preview_exports, { previewCommand: () => previewCommand }); import { promises as fs3 } from "fs"; import chalk2 from "chalk"; import ora2 from "ora"; async function previewCommand(scriptPath) { console.log(chalk2.cyan("\u{1F50D} Statusline Preview Mode\n")); let script; try { const spinner = ora2(`Loading statusline script from ${scriptPath}...`).start(); script = await fs3.readFile(scriptPath, "utf-8"); spinner.succeed("Script loaded!"); const headerMatch = script.match(/# Theme: (\w+) \| Colors: (\w+) \| Features: ([^\n]+)/i); if (headerMatch) { console.log(chalk2.yellow("Detected Configuration:")); console.log(` Theme: ${headerMatch[1]}`); console.log(` Colors: ${headerMatch[2]}`); console.log(` Features: ${headerMatch[3]} `); } const generationMatch = script.match(/# Generated by cc-statusline.*\n# Custom Claude Code statusline - Created: ([^\n]+)/i); if (generationMatch) { console.log(chalk2.gray(`Generated: ${generationMatch[1]} `)); } } catch (error) { console.error(chalk2.red(`\u274C Failed to load script: ${error instanceof Error ? error.message : String(error)}`)); return; } const testSpinner = ora2("Testing statusline with mock data...").start(); const mockInput = generateMockClaudeInput(); console.log(chalk2.gray("\nMock Claude Code Input:")); console.log(chalk2.gray(JSON.stringify(mockInput, null, 2))); const testResult = await testStatuslineScript(script, mockInput); if (testResult.success) { testSpinner.succeed(`Test completed in ${testResult.executionTime}ms`); console.log(chalk2.green("\n\u2705 Statusline Output:")); console.log(chalk2.white("\u2501".repeat(60))); console.log(testResult.output); console.log(chalk2.white("\u2501".repeat(60))); console.log(chalk2.cyan(` \u{1F4CA} Performance: ${getPerformanceEmoji(getPerformanceLevel(testResult.executionTime))} ${getPerformanceLevel(testResult.executionTime)} (${testResult.executionTime}ms)`)); if (testResult.output.includes("\u{1F4C1}") || testResult.output.includes("\u{1F33F}") || testResult.output.includes("\u{1F916}")) { console.log(chalk2.green("\u2705 Statusline features appear to be working")); } else { console.log(chalk2.yellow("\u26A0\uFE0F Basic features may not be displaying correctly")); } } else { testSpinner.fail("Test failed"); console.error(chalk2.red(` \u274C Error: ${testResult.error}`)); if (testResult.output) { console.log(chalk2.gray("\nPartial output:")); console.log(testResult.output); } } console.log(chalk2.green("\n\u2728 Preview complete! Use `cc-statusline init` to generate a new statusline.")); } function getPerformanceEmoji(performance) { switch (performance) { case "excellent": return "\u{1F680}"; case "good": return "\u2705"; case "slow": return "\u26A0\uFE0F"; case "timeout": return "\u{1F40C}"; default: return "\u2753"; } } function getPerformanceLevel(executionTime) { if (executionTime > 1e3) return "timeout"; if (executionTime > 500) return "slow"; if (executionTime > 100) return "good"; return "excellent"; } var init_preview = __esm({ "src/cli/preview.ts"() { "use strict"; init_esm_shims(); init_tester(); } }); // src/index.ts init_esm_shims(); import { Command } from "commander"; // src/cli/commands.ts init_esm_shims(); // src/cli/prompts.ts init_esm_shims(); import inquirer from "inquirer"; async function collectConfiguration() { console.log("\u{1F680} Welcome to cc-statusline! Let's create your custom Claude Code statusline.\n"); console.log("\u2728 All features are enabled by default. Use \u2191/\u2193 arrows to navigate, SPACE to toggle, ENTER to continue.\n"); const config = await inquirer.prompt([ { type: "checkbox", name: "features", message: "Select statusline features (scroll down for more options):", choices: [ { name: "\u{1F4C1} Working Directory", value: "directory", checked: true }, { name: "\u{1F33F} Git Branch", value: "git", checked: true }, { name: "\u{1F916} Model Name & Version", value: "model", checked: true }, { name: "\u{1F9E0} Context Remaining", value: "context", checked: true }, { name: "\u{1F4B5} Usage & Cost", value: "usage", checked: true }, { name: "\u{1F4CA} Token Statistics", value: "tokens", checked: true }, { name: "\u26A1 Burn Rate ($/hr & tokens/min)", value: "burnrate", checked: true }, { name: "\u231B Session Reset Time (requires ccusage)", value: "session", checked: false } ], validate: (answer) => { if (answer.length < 1) { return "You must choose at least one feature."; } return true; }, pageSize: 10 }, { type: "confirm", name: "colors", message: "\n\u{1F3A8} Enable modern color scheme and emojis?", default: true }, { type: "confirm", name: "logging", message: "\n\u{1F4DD} Enable debug logging to .claude/statusline.log?", default: false }, { type: "list", name: "installLocation", message: "\n\u{1F4CD} Where would you like to install the statusline?", choices: [ { name: "\u{1F3E0} Global (~/.claude) - Use across all projects", value: "global" }, { name: "\u{1F4C2} Project (./.claude) - Only for this project", value: "project" } ], default: "project" } ]); const needsCcusage = config.features.includes("session"); return { features: config.features, runtime: "bash", colors: config.colors, theme: "detailed", ccusageIntegration: needsCcusage, logging: config.logging, customEmojis: false, installLocation: config.installLocation }; } // src/generators/bash-generator.ts init_esm_shims(); // src/features/colors.ts init_esm_shims(); function generateColorBashCode(config) { if (!config.enabled) { return ` # ---- color helpers (disabled) ---- use_color=0 C() { :; } RST() { :; } `; } return ` # ---- color helpers (force colors for Claude Code) ---- use_color=1 [ -n "$NO_COLOR" ] && use_color=0 C() { if [ "$use_color" -eq 1 ]; then printf '\\033[%sm' "$1"; fi; } RST() { if [ "$use_color" -eq 1 ]; then printf '\\033[0m'; fi; } `; } function generateBasicColors() { return ` # ---- modern sleek colors ---- dir_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;117m'; fi; } # sky blue model_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;147m'; fi; } # light purple version_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;180m'; fi; } # soft yellow cc_version_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;249m'; fi; } # light gray style_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;245m'; fi; } # gray rst() { if [ "$use_color" -eq 1 ]; then printf '\\033[0m'; fi; } `; } // src/features/git.ts init_esm_shims(); function generateGitBashCode(config, colors) { if (!config.enabled) return ""; const colorCode = colors ? ` # ---- git colors ---- git_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;150m'; fi; } # soft green rst() { if [ "$use_color" -eq 1 ]; then printf '\\033[0m'; fi; } ` : ` git_color() { :; } rst() { :; } `; return `${colorCode} # ---- git ---- git_branch="" if git rev-parse --git-dir >/dev/null 2>&1; then git_branch=$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null) fi`; } function generateGitUtilities() { return ` # git utilities num_or_zero() { v="$1"; [[ "$v" =~ ^[0-9]+$ ]] && echo "$v" || echo 0; }`; } // src/features/usage.ts init_esm_shims(); function generateUsageBashCode(config, colors) { if (!config.enabled) return ""; const colorCode = colors ? ` # ---- usage colors ---- usage_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;189m'; fi; } # lavender cost_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;222m'; fi; } # light gold burn_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;220m'; fi; } # bright gold session_color() { rem_pct=$(( 100 - session_pct )) if (( rem_pct <= 10 )); then SCLR='38;5;210' # light pink elif (( rem_pct <= 25 )); then SCLR='38;5;228' # light yellow else SCLR='38;5;194'; fi # light green if [ "$use_color" -eq 1 ]; then printf '\\033[%sm' "$SCLR"; fi } ` : ` usage_color() { :; } cost_color() { :; } burn_color() { :; } session_color() { :; } `; const needsCcusage = config.showSession || config.showProgressBar; return `${colorCode} # ---- cost and usage extraction ---- session_txt=""; session_pct=0; session_bar="" cost_usd=""; cost_per_hour=""; tpm=""; tot_tokens="" # Extract cost and token data from Claude Code's native input if [ "$HAS_JQ" -eq 1 ]; then # Cost data cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty' 2>/dev/null) total_duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // empty' 2>/dev/null) # Calculate burn rate ($/hour) from cost and duration if [ -n "$cost_usd" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then cost_per_hour=$(echo "$cost_usd $total_duration_ms" | awk '{printf "%.2f", $1 * 3600000 / $2}') fi ${config.showTokens ? ` # Token data from native context_window (no ccusage needed) input_tokens=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0' 2>/dev/null) output_tokens=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0' 2>/dev/null) if [ "$input_tokens" != "null" ] && [ "$output_tokens" != "null" ]; then tot_tokens=$(( input_tokens + output_tokens )) [ "$tot_tokens" -eq 0 ] && tot_tokens="" fi` : ""} ${config.showBurnRate && config.showTokens ? ` # Calculate tokens per minute from native data if [ -n "$tot_tokens" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then # Convert ms to minutes and calculate rate tpm=$(echo "$tot_tokens $total_duration_ms" | awk '{if ($2 > 0) printf "%.0f", $1 * 60000 / $2; else print ""}') fi` : ""} else # Bash fallback for cost extraction cost_usd=$(echo "$input" | grep -o '"total_cost_usd"[[:space:]]*:[[:space:]]*[0-9.]*' | sed 's/.*:[[:space:]]*\\([0-9.]*\\).*/\\1/') total_duration_ms=$(echo "$input" | grep -o '"total_duration_ms"[[:space:]]*:[[:space:]]*[0-9]*' | sed 's/.*:[[:space:]]*\\([0-9]*\\).*/\\1/') # Calculate burn rate ($/hour) from cost and duration if [ -n "$cost_usd" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then cost_per_hour=$(echo "$cost_usd $total_duration_ms" | awk '{printf "%.2f", $1 * 3600000 / $2}') fi ${config.showTokens ? ` # Token data from native context_window (bash fallback) input_tokens=$(echo "$input" | grep -o '"total_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' | sed 's/.*:[[:space:]]*\\([0-9]*\\).*/\\1/') output_tokens=$(echo "$input" | grep -o '"total_output_tokens"[[:space:]]*:[[:space:]]*[0-9]*' | sed 's/.*:[[:space:]]*\\([0-9]*\\).*/\\1/') if [ -n "$input_tokens" ] && [ -n "$output_tokens" ]; then tot_tokens=$(( input_tokens + output_tokens )) [ "$tot_tokens" -eq 0 ] && tot_tokens="" fi` : ""} ${config.showBurnRate && config.showTokens ? ` # Calculate tokens per minute from native data if [ -n "$tot_tokens" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then tpm=$(echo "$tot_tokens $total_duration_ms" | awk '{if ($2 > 0) printf "%.0f", $1 * 60000 / $2; else print ""}') fi` : ""} fi ${needsCcusage ? ` # Session reset time requires ccusage (only feature that needs external tool) if command -v ccusage >/dev/null 2>&1 && [ "$HAS_JQ" -eq 1 ]; then blocks_output="" # Try ccusage with timeout if command -v timeout >/dev/null 2>&1; then blocks_output=$(timeout 5s ccusage blocks --json 2>/dev/null) elif command -v gtimeout >/dev/null 2>&1; then blocks_output=$(gtimeout 5s ccusage blocks --json 2>/dev/null) else blocks_output=$(ccusage blocks --json 2>/dev/null) fi if [ -n "$blocks_output" ]; then active_block=$(echo "$blocks_output" | jq -c '.blocks[] | select(.isActive == true)' 2>/dev/null | head -n1) if [ -n "$active_block" ]; then # Session time calculation from ccusage reset_time_str=$(echo "$active_block" | jq -r '.usageLimitResetTime // .endTime // empty') start_time_str=$(echo "$active_block" | jq -r '.startTime // empty') if [ -n "$reset_time_str" ] && [ -n "$start_time_str" ]; then start_sec=$(to_epoch "$start_time_str"); end_sec=$(to_epoch "$reset_time_str"); now_sec=$(date +%s) total=$(( end_sec - start_sec )); (( total<1 )) && total=1 elapsed=$(( now_sec - start_sec )); (( elapsed<0 ))&&elapsed=0; (( elapsed>total ))&&elapsed=$total session_pct=$(( elapsed * 100 / total )) remaining=$(( end_sec - now_sec )); (( remaining<0 )) && remaining=0 rh=$(( remaining / 3600 )); rm=$(( (remaining % 3600) / 60 )) end_hm=$(fmt_time_hm "$end_sec")${config.showSession ? ` session_txt="$(printf '%dh %dm until reset at %s (%d%%)' "$rh" "$rm" "$end_hm" "$session_pct")"` : ""}${config.showProgressBar ? ` session_bar=$(progress_bar "$session_pct" 10)` : ""} fi fi fi fi` : ""}`; } function generateUsageUtilities() { return ` # ---- time helpers ---- to_epoch() { ts="$1" if command -v gdate >/dev/null 2>&1; then gdate -d "$ts" +%s 2>/dev/null && return; fi date -u -j -f "%Y-%m-%dT%H:%M:%S%z" "\${ts/Z/+0000}" +%s 2>/dev/null && return python3 - "$ts" <<'PY' 2>/dev/null import sys, datetime s=sys.argv[1].replace('Z','+00:00') print(int(datetime.datetime.fromisoformat(s).timestamp())) PY } fmt_time_hm() { epoch="$1" if date -r 0 +%s >/dev/null 2>&1; then date -r "$epoch" +"%H:%M"; else date -d "@$epoch" +"%H:%M"; fi } progress_bar() { pct="\${1:-0}"; width="\${2:-10}" [[ "$pct" =~ ^[0-9]+$ ]] || pct=0; ((pct<0))&&pct=0; ((pct>100))&&pct=100 filled=$(( pct * width / 100 )); empty=$(( width - filled )) printf '%*s' "$filled" '' | tr ' ' '=' printf '%*s' "$empty" '' | tr ' ' '-' }`; } // src/generators/bash-generator.ts var VERSION = "1.4.0"; function generateBashStatusline(config) { const hasGit = config.features.includes("git"); const hasUsage = config.features.some((f) => ["usage", "session", "tokens", "burnrate"].includes(f)); const hasDirectory = config.features.includes("directory"); const hasModel = config.features.includes("model"); const hasContext = config.features.includes("context"); const usageConfig = { enabled: hasUsage && config.ccusageIntegration, showCost: config.features.includes("usage"), showTokens: config.features.includes("tokens"), showBurnRate: config.features.includes("burnrate"), showSession: config.features.includes("session"), showProgressBar: config.theme !== "minimal" && config.features.includes("session") }; const gitConfig = { enabled: hasGit, showBranch: hasGit, showChanges: false, // Removed delta changes per user request compactMode: config.theme === "compact" }; const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const script = `#!/bin/bash # Generated by cc-statusline v${VERSION} (https://www.npmjs.com/package/@chongdashu/cc-statusline) # Custom Claude Code statusline - Created: ${timestamp} # Theme: ${config.theme} | Colors: ${config.colors} | Features: ${config.features.join(", ")} STATUSLINE_VERSION="${VERSION}" input=$(cat) # ---- check jq availability ---- HAS_JQ=0 if command -v jq >/dev/null 2>&1; then HAS_JQ=1 fi ${config.logging ? generateLoggingCode() : ""} ${generateColorBashCode({ enabled: config.colors, theme: config.theme })} ${config.colors ? generateBasicColors() : ""} ${hasUsage ? generateUsageUtilities() : ""} ${hasGit ? generateGitUtilities() : ""} ${generateBasicDataExtraction(hasDirectory, hasModel, hasContext)} ${hasGit ? generateGitBashCode(gitConfig, config.colors) : ""} ${hasContext ? generateContextBashCode(config.colors) : ""} ${hasUsage ? generateUsageBashCode(usageConfig, config.colors) : ""} ${config.logging ? generateLoggingOutput() : ""} ${generateDisplaySection(config, gitConfig, usageConfig)} `; return script.replace(/\n\n\n+/g, "\n\n").trim() + "\n"; } function generateLoggingCode() { return ` # Get the directory where this statusline script is located SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" LOG_FILE="\${SCRIPT_DIR}/statusline.log" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') # ---- logging ---- { echo "[$TIMESTAMP] Status line triggered (cc-statusline v\${STATUSLINE_VERSION})" echo "[$TIMESTAMP] Input:" if [ "$HAS_JQ" -eq 1 ]; then echo "$input" | jq . 2>/dev/null || echo "$input" echo "[$TIMESTAMP] Using jq for JSON parsing" else echo "$input" echo "[$TIMESTAMP] WARNING: jq not found, using bash fallback for JSON parsing" fi echo "---" } >> "$LOG_FILE" 2>/dev/null `; } function generateJsonExtractorCode() { return ` # ---- JSON extraction utilities ---- # Pure bash JSON value extractor (fallback when jq not available) extract_json_string() { local json="$1" local key="$2" local default="\${3:-}" # For nested keys like workspace.current_dir, get the last part local field="\${key##*.}" field="\${field%% *}" # Remove any jq operators # Try to extract string value (quoted) local value=$(echo "$json" | grep -o "\\"\\\${field}\\"[[:space:]]*:[[:space:]]*\\"[^\\"]*\\"" | head -1 | sed 's/.*:[[:space:]]*"\\([^"]*\\)".*/\\1/') # Convert escaped backslashes to forward slashes for Windows paths if [ -n "$value" ]; then value=$(echo "$value" | sed 's/\\\\\\\\/\\//g') fi # If no string value found, try to extract number value (unquoted) if [ -z "$value" ] || [ "$value" = "null" ]; then value=$(echo "$json" | grep -o "\\"\\\${field}\\"[[:space:]]*:[[:space:]]*[0-9.]\\+" | head -1 | sed 's/.*:[[:space:]]*\\([0-9.]\\+\\).*/\\1/') fi # Return value or default if [ -n "$value" ] && [ "$value" != "null" ]; then echo "$value" else echo "$default" fi } `; } function generateBasicDataExtraction(hasDirectory, hasModel, hasContext) { return ` ${generateJsonExtractorCode()} # ---- basics ---- if [ "$HAS_JQ" -eq 1 ]; then${hasDirectory ? ` current_dir=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "unknown"' 2>/dev/null | sed "s|^$HOME|~|g")` : ""}${hasModel ? ` model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null) model_version=$(echo "$input" | jq -r '.model.version // ""' 2>/dev/null)` : ""}${hasContext ? ` session_id=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)` : ""} cc_version=$(echo "$input" | jq -r '.version // ""' 2>/dev/null) output_style=$(echo "$input" | jq -r '.output_style.name // ""' 2>/dev/null) else${hasDirectory ? ` # Bash fallback for JSON extraction # Extract current_dir from workspace object - look for the pattern workspace":{"current_dir":"..."} current_dir=$(echo "$input" | grep -o '"workspace"[[:space:]]*:[[:space:]]*{[^}]*"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/' | sed 's/\\\\\\\\/\\//g') # Fall back to cwd if workspace extraction failed if [ -z "$current_dir" ] || [ "$current_dir" = "null" ]; then current_dir=$(echo "$input" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/' | sed 's/\\\\\\\\/\\//g') fi # Fallback to unknown if all extraction failed [ -z "$current_dir" ] && current_dir="unknown" current_dir=$(echo "$current_dir" | sed "s|^$HOME|~|g")` : ""}${hasModel ? ` # Extract model name from nested model object model_name=$(echo "$input" | grep -o '"model"[[:space:]]*:[[:space:]]*{[^}]*"display_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"display_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/') [ -z "$model_name" ] && model_name="Claude" # Model version is in the model ID, not a separate field model_version="" # Not available in Claude Code JSON` : ""}${hasContext ? ` session_id=$(extract_json_string "$input" "session_id" "")` : ""} # CC version is at the root level cc_version=$(echo "$input" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/') # Output style is nested output_style=$(echo "$input" | grep -o '"output_style"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/') fi `; } function generateContextBashCode(colors) { return ` # ---- context window calculation (native) ---- context_pct="" context_remaining_pct="" context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[1;37m'; fi; } # default white if [ "$HAS_JQ" -eq 1 ]; then # Get context window size and current usage from native Claude Code input CONTEXT_SIZE=$(echo "$input" | jq -r '.context_window.context_window_size // 200000' 2>/dev/null) USAGE=$(echo "$input" | jq '.context_window.current_usage' 2>/dev/null) if [ "$USAGE" != "null" ] && [ -n "$USAGE" ]; then # Calculate current context from current_usage fields # Formula: input_tokens + cache_creation_input_tokens + cache_read_input_tokens CURRENT_TOKENS=$(echo "$USAGE" | jq '(.input_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0)' 2>/dev/null) if [ -n "$CURRENT_TOKENS" ] && [ "$CURRENT_TOKENS" -gt 0 ] 2>/dev/null; then context_used_pct=$(( CURRENT_TOKENS * 100 / CONTEXT_SIZE )) context_remaining_pct=$(( 100 - context_used_pct )) # Clamp to valid range (( context_remaining_pct < 0 )) && context_remaining_pct=0 (( context_remaining_pct > 100 )) && context_remaining_pct=100 # Set color based on remaining percentage if [ "$context_remaining_pct" -le 20 ]; then context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;203m'; fi; } # coral red elif [ "$context_remaining_pct" -le 40 ]; then context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;215m'; fi; } # peach else context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;158m'; fi; } # mint green fi context_pct="\${context_remaining_pct}%" fi fi fi `; } function generateLoggingOutput() { return ` # ---- log extracted data ---- { 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:-}" if [ "$HAS_JQ" -eq 0 ]; then echo "[$TIMESTAMP] Note: Context, tokens, and session info require jq for full functionality" fi } >> "$LOG_FILE" 2>/dev/null `; } function generateDisplaySection(config, gitConfig, usageConfig) { const emojis = config.colors && !config.customEmojis; return ` # ---- render statusline ---- # Line 1: Core info (directory, git, model, claude code version, output style) ${config.features.includes("directory") ? `printf '\u{1F4C1} %s%s%s' "$(dir_color)" "$current_dir" "$(rst)"` : ""}${gitConfig.enabled ? ` if [ -n "$git_branch" ]; then printf ' \u{1F33F} %s%s%s' "$(git_color)" "$git_branch" "$(rst)" fi` : ""}${config.features.includes("model") ? ` printf ' \u{1F916} %s%s%s' "$(model_color)" "$model_name" "$(rst)" if [ -n "$model_version" ] && [ "$model_version" != "null" ]; then printf ' \u{1F3F7}\uFE0F %s%s%s' "$(version_color)" "$model_version" "$(rst)" fi` : ""} if [ -n "$cc_version" ] && [ "$cc_version" != "null" ]; then printf ' \u{1F4DF} %sv%s%s' "$(cc_version_color)" "$cc_version" "$(rst)" fi if [ -n "$output_style" ] && [ "$output_style" != "null" ]; then printf ' \u{1F3A8} %s%s%s' "$(style_color)" "$output_style" "$(rst)" fi # Line 2: Context and session time line2=""${config.features.includes("context") ? ` if [ -n "$context_pct" ]; then context_bar=$(progress_bar "$context_remaining_pct" 10) line2="\u{1F9E0} $(context_color)Context Remaining: \${context_pct} [\${context_bar}]$(rst)" fi` : ""}${usageConfig.showSession ? ` if [ -n "$session_txt" ]; then if [ -n "$line2" ]; then line2="$line2 \u231B $(session_color)\${session_txt}$(rst) $(session_color)[\${session_bar}]$(rst)" else line2="\u231B $(session_color)\${session_txt}$(rst) $(session_color)[\${session_bar}]$(rst)" fi fi` : ""}${config.features.includes("context") ? ` if [ -z "$line2" ] && [ -z "$context_pct" ]; then line2="\u{1F9E0} $(context_color)Context Remaining: TBD$(rst)" fi` : ""} # Line 3: Cost and usage analytics line3=""${usageConfig.showCost ? ` if [ -n "$cost_usd" ] && [[ "$cost_usd" =~ ^[0-9.]+$ ]]; then${usageConfig.showBurnRate ? ` if [ -n "$cost_per_hour" ] && [[ "$cost_per_hour" =~ ^[0-9.]+$ ]]; then cost_per_hour_formatted=$(printf '%.2f' "$cost_per_hour") line3="\u{1F4B0} $(cost_color)\\$$(printf '%.2f' "$cost_usd")$(rst) ($(burn_color)\\$\${cost_per_hour_formatted}/h$(rst))" else line3="\u{1F4B0} $(cost_color)\\$$(printf '%.2f' "$cost_usd")$(rst)" fi` : ` line3="\u{1F4B0} $(cost_color)\\$$(printf '%.2f' "$cost_usd")$(rst)"`} fi` : ""}${usageConfig.showTokens ? ` if [ -n "$tot_tokens" ] && [[ "$tot_tokens" =~ ^[0-9]+$ ]]; then${usageConfig.showBurnRate ? ` if [ -n "$tpm" ] && [[ "$tpm" =~ ^[0-9.]+$ ]]; then tpm_formatted=$(printf '%.0f' "$tpm") if [ -n "$line3" ]; then line3="$line3 \u{1F4CA} $(usage_color)\${tot_tokens} tok (\${tpm_formatted} tpm)$(rst)" else line3="\u{1F4CA} $(usage_color)\${tot_tokens} tok (\${tpm_formatted} tpm)$(rst)" fi else if [ -n "$line3" ]; then line3="$line3 \u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" else line3="\u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" fi fi` : ` if [ -n "$line3" ]; then line3="$line3 \u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" else line3="\u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" fi`} fi` : ""} # Print lines if [ -n "$line2" ]; then printf '\\n%s' "$line2" fi if [ -n "$line3" ]; then printf '\\n%s' "$line3" fi printf '\\n'`; } // src/utils/validator.ts init_esm_shims(); function validateConfig(config) { const errors = []; const warnings = []; if (!config.features || config.features.length === 0) { errors.push("At least one display feature must be selected"); } if (!["bash", "python", "node"].includes(config.runtime)) { errors.push(`Invalid runtime: ${config.runtime}`); } if (!["minimal", "detailed", "compact"].includes(config.theme)) { errors.push(`Invalid theme: ${config.theme}`); } const usageFeatures = ["usage", "session", "tokens", "burnrate"]; const hasUsageFeatures = config.features.some((f) => usageFeatures.includes(f)); if (hasUsageFeatures && !config.ccusageIntegration) { warnings.push("Usage features selected but ccusage integration is disabled. Some features may not work properly."); } if (config.features.length > 5) { warnings.push("Many features selected. This may impact statusline performance."); } if (config.customEmojis && !config.colors) { warnings.push("Custom emojis enabled but colors disabled. Visual distinction may be limited."); } return { isValid: errors.length === 0, errors, warnings }; } // src/utils/installer.ts init_esm_shims(); import { promises as fs } from "fs"; import path2 from "path"; import os from "os"; import inquirer2 from "inquirer"; async function installStatusline(script, outputPath, config) { try { const isGlobal = config.installLocation === "global"; const claudeDir = isGlobal ? path2.join(os.homedir(), ".claude") : "./.claude"; const scriptPath = path2.join(claudeDir, "statusline.sh"); await fs.mkdir(claudeDir, { recursive: true }); let shouldWrite = true; try { await fs.access(scriptPath); const { confirmOverwrite } = await inquirer2.prompt([{ type: "confirm", name: "confirmOverwrite", message: `\u26A0\uFE0F ${isGlobal ? "Global" : "Project"} statusline.sh already exists. Overwrite?`, default: false }]); shouldWrite = confirmOverwrite; } catch { } if (shouldWrite) { await fs.writeFile(scriptPath, script, { mode: 493 }); } else { throw new Error("USER_CANCELLED_OVERWRITE"); } await updateSettingsJson(claudeDir, "statusline.sh", isGlobal); } catch (error) { throw new Error(`Failed to install statusline: ${error instanceof Error ? error.message : String(error)}`); } } async function updateSettingsJson(claudeDir, scriptName, isGlobal) { var _a; const settingsPath = path2.join(claudeDir, "settings.json"); try { let settings = {}; let existingStatusLine = null; try { const settingsContent = await fs.readFile(settingsPath, "utf-8"); settings = JSON.parse(settingsContent); existingStatusLine = settings.statusLine; } catch { } if (existingStatusLine && existingStatusLine.command) { const isOurStatusline = (_a = existingStatusLine.command) == null ? void 0 : _a.includes("statusline.sh"); if (!isOurStatusline) { const { confirmReplace } = await inquirer2.prompt([{ type: "confirm", name: "confirmReplace", message: `\u26A0\uFE0F ${isGlobal ? "Global" : "Project"} settings.json already has a statusLine configured (${existingStatusLine.command}). Replace it?`, default: false }]); if (!confirmReplace) { console.warn("\n\u26A0\uFE0F Statusline script was saved but settings.json was not updated."); console.warn(" Your existing statusLine configuration was preserved."); return; } } } const commandPath = process.platform === "win32" ? `bash ${isGlobal ? ".claude" : ".claude"}/${scriptName}` : isGlobal ? `~/.claude/${scriptName}` : `.claude/${scriptName}`; settings.statusLine = { type: "command", command: commandPath, padding: 0 }; await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); } catch (error) { console.warn(`Warning: Could not update settings.json: ${error instanceof Error ? error.message : String(error)}`); throw new Error("SETTINGS_UPDATE_FAILED"); } } // src/cli/commands.ts import chalk from "chalk"; import ora from "ora"; import path4 from "path"; import os2 from "os"; import { execSync } from "child_process"; function checkJqInstallation() { try { execSync("command -v jq", { stdio: "ignore" }); return true; } catch { return false; } } function getJqInstallInstructions() { const platform = process.platform; if (platform === "darwin") { return ` ${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")} ${chalk.green("Using Homebrew (recommended):")} brew install jq ${chalk.green("Using MacPorts:")} sudo port install jq ${chalk.green("Or download directly:")} https://github.com/jqlang/jq/releases `; } else if (platform === "linux") { return ` ${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")} ${chalk.green("Ubuntu/Debian:")} sudo apt-get install jq ${chalk.green("CentOS/RHEL/Fedora:")} sudo yum install jq ${chalk.green("Arch Linux:")} sudo pacman -S jq ${chalk.green("Or download directly:")} https://github.com/jqlang/jq/releases `; } else if (platform === "win32") { return ` ${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")} ${chalk.green("Option 1: Using Package Manager")} ${chalk.dim("Chocolatey:")} choco install jq ${chalk.dim("Scoop:")} scoop install jq ${chalk.green("Option 2: Manual Download")} 1. Download from: https://github.com/jqlang/jq/releases/latest 2. Choose file: ${chalk.dim("\u2022 64-bit Windows:")} jq-windows-amd64.exe ${chalk.dim("\u2022 32-bit Windows:")} jq-windows-i386.exe 3. Rename to: jq.exe 4. Move to: C:\\Windows\\System32\\ ${chalk.dim("(or add to PATH)")} 5. Test: Open new terminal and run: jq --version `; } else { return ` ${chalk.cyan("\u{1F4E6} Install jq for better performance and reliability:")} ${chalk.green("Download for your platform:")} https://github.com/jqlang/jq/releases `; } } async function initCommand(options) { try { const spinner = ora("Initializing statusline generator...").start(); await new Promise((resolve) => setTimeout(resolve, 500)); spinner.stop(); const hasJq = checkJqInstallation(); if (!hasJq) { console.log(chalk.yellow("\n\u26A0\uFE0F jq is not installed")); console.log(chalk.dim("Your statusline will work without jq, but with limited functionality:")); console.log(chalk.dim(" \u2022 Context remaining percentage won't be displayed")); console.log(chalk.dim(" \u2022 Token statistics may not work")); console.log(chalk.dim(" \u2022 Performance will be slower")); console.log(getJqInstallInstructions()); const inquirer3 = (await import("inquirer")).default; const { continueWithoutJq } = await inquirer3.prompt([{ type: "confirm", name: "continueWithoutJq", message: "Continue without jq?", default: true }]); if (!continueWithoutJq) { console.log(chalk.cyan("\n\u{1F44D} Install jq and run this command again")); process.exit(0); } } const config = await collectConfiguration(); const validation = validateConfig(config); if (!validation.isValid) { console.error(chalk.red("\u274C Configuration validation failed:")); validation.errors.forEach((error) => console.error(chalk.red(` \u2022 ${error}`))); process.exit(1); } const generationSpinner = ora("Generating statusline script...").start(); const script = generateBashStatusline(config); const filename = "statusline.sh"; generationSpinner.succeed("Statusline script generated!"); console.log(chalk.cyan("\n\u2728 Your statusline will look like:")); console.log(chalk.white("\u2501".repeat(60))); const { testStatuslineScript: testStatuslineScript2, generateMockClaudeInput: generateMockClaudeInput2 } = await Promise.resolve().then(() => (init_tester(), tester_exports)); const mockInput = generateMockClaudeInput2(); const testResult = await testStatuslineScript2(script, mockInput); if (testResult.success) { console.log(testResult.output); } else { console.log(chalk.gray("\u{1F4C1} ~/projects/my-app \u{1F33F} main \u{1F916} Claude \u{1F4B5} $2.48 ($12.50/h)")); console.log(chalk.gray("(Preview unavailable - will work when Claude Code runs it)")); } console.log(chalk.white("\u2501".repeat(60))); const isGlobal = config.installLocation === "global"; const baseDir = isGlobal ? os2.homedir() : "."; const outputPath = options.output || path4.join(baseDir, ".claude", filename); const resolvedPath = path4.resolve(outputPath); if (options.install !== false) { console.log(chalk.cyan("\n\u{1F4E6} Installing statusline...")); try { await installStatusline(script, resolvedPath, config); console.log(chalk.green("\n\u2705 Statusline installed!")); console.log(chalk.green("\n\u{1F389} Success! Your custom statusline is ready!")); console.log(chalk.cyan(` \u{1F4C1} ${isGlobal ? "Global" : "Project"} installation complete: ${chalk.white(resolvedPath)}`)); console.log(chalk.cyan("\nNext steps:")); console.log(chalk.white(" 1. Restart Claude Code to see your new statusline")); if (config.features.includes("session")) { console.log(chalk.white(" 2. Session reset time requires ccusage: npx ccusage@latest")); } } catch (error) { console.log(chalk.red("\n\u274C Failed to install statusline")); if (error instanceof Error && error.message === "USER_CANCELLED_OVERWRITE") { console.log(chalk.yellow("\n\u26A0\uFE0F Installation cancelled. Existing statusline.sh was not overwritten.")); } else if (error instanceof Error && error.message === "SETTINGS_UPDATE_FAILED") { const commandPath = isGlobal ? "~/.claude/statusline.sh" : ".claude/statusline.sh"; console.log(chalk.yellow("\n\u26A0\uFE0F Settings.json could not be updated automatically.")); console.log(chalk.cyan("\nManual Configuration Required:")); console.log(chalk.white(`Add this to your ${isGlobal ? "~/.claude" : ".claude"}/settings.json file:`)); console.log(chalk.gray("\n{")); console.log(chalk.gray(' "statusLine": {')); console.log(chalk.gray(' "type": "command",')); console.log(chalk.gray(` "command": "${commandPath}",`)); console.log(chalk.gray(' "padding": 0')); console.log(chalk.gray(" }")); console.log(chalk.gray("}")); console.log(chalk.cyan(` \u{1F4C1} Statusline script saved to: ${chalk.white(resolvedPath)}`)); } else { console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`)); console.log(chalk.cyan(` \u{1F4C1} You can manually save the script to: ${chalk.white(resolvedPath)}`)); } } } else { console.log(chalk.green("\n\u2705 Statusline generated successfully!")); console.log(chalk.cyan(` \u{1F4C1} Save this script to: ${chalk.white(resolvedPath)}`)); console.log(chalk.cyan("\nThen restart Claude Code to see your new statusline.")); } } catch (error) { console.error(chalk.red("\u274C An error occurred:")); console.error(chalk.red(error instanceof Error ? error.message : String(error))); process.exit(1); } } // src/index.ts import chalk3 from "chalk"; var VERSION2 = "1.4.0"; var program = new Command(); program.name("cc-statusline").description("Interactive CLI tool for generating custom Claude Code statuslines").version(VERSION2); 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); program.command("preview").description("Preview existing statusline.sh with mock data").argument("<script-path>", "Path to statusline.sh file to preview").action(async (scriptPath) => { const { previewCommand: previewCommand2 } = await Promise.resolve().then(() => (init_preview(), preview_exports)); await previewCommand2(scriptPath); }); program.command("test").description("Test statusline with real Claude Code JSON input").option("-c, --config <path>", "Configuration file to test").action(() => { console.log(chalk3.yellow("Test command coming soon!")); }); if (!process.argv.slice(2).length) { program.outputHelp(); } program.parse(process.argv); //# sourceMappingURL=index.js.map