From e7e285bb4c36605d2aa8791291664a122d7affb1 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:55:55 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 13 ++ README.md | 3 + commands/install-statusline.md | 300 +++++++++++++++++++++++++++++++++ commands/preview-statusline.md | 54 ++++++ plugin.lock.json | 49 ++++++ 5 files changed, 419 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/install-statusline.md create mode 100644 commands/preview-statusline.md create mode 100644 plugin.lock.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..7351e7f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "statusline", + "description": "Installs a status line script for Claude Code showing branch, model, cost, duration, diff lines, and an optional quote.", + "version": "1.1.1", + "author": { + "name": "Kazuki Hashimoto", + "email": "setouchi.develop@gmail.com" + }, + "commands": [ + "./commands/install-statusline.md", + "./commands/preview-statusline.md" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2f7fc0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# statusline + +Installs a status line script for Claude Code showing branch, model, cost, duration, diff lines, and an optional quote. diff --git a/commands/install-statusline.md b/commands/install-statusline.md new file mode 100644 index 0000000..9d247b9 --- /dev/null +++ b/commands/install-statusline.md @@ -0,0 +1,300 @@ +--- +description: Install the Claude Code status line shell script to ~/.claude/scripts/statusline.sh +argument-hint: "[--force] [--no-quotes]" +allowed-tools: [Bash(mkdir:*), Bash(tee:*), Bash(chmod:*), Bash(which:*), Bash(echo:*), Bash(test:*), Bash(cat:*), Bash(jq:*), Bash(mv:*)] +--- + +# Install Statusline + +This command writes a shell script to `~/.claude/scripts/statusline.sh` that renders a rich status line for Claude Code sessions (branch, model, cost, duration, lines changed, and optionally a quote). + +## Behavior + +- If the target file exists and `--force` is not provided, confirm before overwriting. +- Ensures the `~/.claude/scripts` directory exists and sets the script executable. +- Verifies `jq` is available (required by the script) and warns if missing. +- Automatically configures `~/.claude/settings.json` to enable the status line. +- Supports `--no-quotes` to hide the quote from the status line. +- If `statusLine` is already configured and `--force` is not provided, skips configuration update. + +## Steps + +1) Check for `jq` and warn if not present (continue anyway): + +- Run: `which jq || echo "Warning: jq not found. Please install jq to enable the status line parser."` + +2) Create the scripts directory: + +- Run: `mkdir -p ~/.claude/scripts` + +3) If the file exists and `--force` is not passed, prompt for confirmation. Otherwise, proceed to write the file using a here-doc with `tee` and then mark it executable. + +- Run: + +``` +test -f ~/.claude/scripts/statusline.sh \ + && ! echo "$ARGUMENTS" | grep -q -- '--force' \ + && echo "~/.claude/scripts/statusline.sh already exists. Re-run with --force to overwrite." \ + && exit 0 || true +tee ~/.claude/scripts/statusline.sh >/dev/null <<'EOF' +#!/usr/bin/env bash + +# Status line script for Claude Code +# Displays: Branch, Model, Cost, Duration, Lines changed, and an optional quote + +# Parse flags +NO_QUOTES=0 +for arg in "$@"; do + case "$arg" in + --no-quotes) + NO_QUOTES=1 + ;; + esac +done + +# Also allow env toggle for flexibility +if [ "${CLAUDE_STATUSLINE_NO_QUOTES:-}" = "1" ]; then + NO_QUOTES=1 +fi + +# Check if jq is installed +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq is required but not installed. Please install jq first." >&2 + exit 1 +fi + +# Read JSON input from stdin +input=$(cat) + +# Validate JSON input +if [ -z "$input" ]; then + echo "Error: No JSON input received from Claude Code." >&2 + exit 1 +fi + +# Extract data from JSON +cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0' 2>/dev/null) +duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // 0' 2>/dev/null) +lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0' 2>/dev/null) +lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0' 2>/dev/null) +model_name=$(echo "$input" | jq -r '.model.display_name // "Sonnet 4.5"' 2>/dev/null) +model_id=$(echo "$input" | jq -r '.model.id // ""' 2>/dev/null) +workspace_dir=$(echo "$input" | jq -r '.workspace.current_dir // .workspace.project_dir // ""' 2>/dev/null) + +# Validate critical values were parsed successfully +if [ -z "$cost" ] || [ "$cost" = "null" ]; then + echo "Error: Failed to parse session data. Invalid or malformed JSON input." >&2 + exit 1 +fi + +# Get current git branch (use workspace directory if available) +if [ -n "$workspace_dir" ] && [ -d "$workspace_dir" ]; then + branch=$(git -C "$workspace_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-branch") +else + branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-branch") +fi + +# Format duration (convert milliseconds to seconds) +if [ "$duration_ms" != "0" ] && [ "$duration_ms" != "null" ]; then + duration_seconds=$((duration_ms / 1000)) + minutes=$((duration_seconds / 60)) + seconds=$((duration_seconds % 60)) + if [ $minutes -gt 0 ]; then + duration_str="${minutes}m${seconds}s" + else + duration_str="${seconds}s" + fi +else + duration_str="0s" +fi + +if [ "$NO_QUOTES" -ne 1 ]; then + # Fetch quote with 5-minute caching + get_quote() { + local cache_file="/tmp/claude_statusline_quote.txt" + local cache_max_age=300 # 5 minutes in seconds + local fallback_quote="\"Code is poetry\" - Anonymous" + + # Check if cache exists and is fresh (less than 5 minutes old) + if [ -f "$cache_file" ]; then + # Portable mtime check: GNU `stat -c %Y` first, then BSD `stat -f %m`. + # Avoid inline arithmetic with command substitution to prevent multi-line issues. + local now mtime cache_age + now=$(date +%s) + if mtime=$(stat -c %Y "$cache_file" 2>/dev/null); then + : + else + mtime=$(stat -f %m "$cache_file" 2>/dev/null || echo 0) + fi + # Ensure numeric value (defensive in case of unexpected output) + mtime=${mtime//[^0-9]/} + [ -z "$mtime" ] && mtime=0 + cache_age=$(( now - mtime )) + + # Use cached quote if it's less than 5 minutes old + if [ "$cache_age" -lt "$cache_max_age" ]; then + cat "$cache_file" + return 0 + fi + fi + + # Cache is stale or doesn't exist - try to fetch new quote from API + local api_response=$(curl -s --max-time 2 "https://zenquotes.io/api/random" 2>/dev/null) + local curl_exit_code=$? + + if [ $curl_exit_code -eq 0 ] && [ -n "$api_response" ]; then + # Parse the JSON response (ZenQuotes returns an array, so use .[0]) + local quote_text=$(echo "$api_response" | jq -r '.[0].q // empty' 2>/dev/null) + local quote_author=$(echo "$api_response" | jq -r '.[0].a // "Unknown"' 2>/dev/null) + + if [ -n "$quote_text" ] && [ "$quote_text" != "null" ] && [ "$quote_text" != "empty" ]; then + # Format quote and save to cache + local formatted_quote="\"$quote_text\" - $quote_author" + echo "$formatted_quote" > "$cache_file" + echo "$formatted_quote" + return 0 + fi + fi + + # API failed - try to use old cached quote as fallback + if [ -f "$cache_file" ]; then + cat "$cache_file" + return 0 + fi + + # No cache available, use fallback + echo "$fallback_quote" + } + + quote=$(get_quote) +fi + +# Prepare model display string (include ID only if available) +if [ -n "$model_id" ] && [ "$model_id" != "null" ]; then + model_display="$model_name \033[2;90m($model_id)\033[0m" +else + model_display="$model_name" +fi + +# Apply dim gray color to each word in the quote to ensure color persists on line wrap +# This workaround applies the color code to each word individually +apply_color_per_word() { + local text="$1" + local color_code="\033[2;90m" + local reset_code="\033[0m" + + # Split the text into words and apply color to each word + local words=($text) + local result="" + local word + + for word in "${words[@]}"; do + result+="${color_code}${word}${reset_code} " + done + + # Remove trailing space and output + echo -n "${result% }" +} + +if [ "$NO_QUOTES" -ne 1 ]; then + colored_quote=$(apply_color_per_word "$quote") + # Build and print status line with emojis, colors, and quote + printf "🌿 \033[1;92m%s\033[0m | 🤖 \033[1;96m%b\033[0m | 💰 \033[1;93m\$%.4f\033[0m | ⏱️ \033[1;97m%s\033[0m | 📝 \033[1;92m+%s\033[0m/\033[1;91m-%s\033[0m | 💬 %b" \ + "$branch" \ + "$model_display" \ + "$cost" \ + "$duration_str" \ + "$lines_added" \ + "$lines_removed" \ + "$colored_quote" +else + # Print without quote segment + printf "🌿 \033[1;92m%s\033[0m | 🤖 \033[1;96m%b\033[0m | 💰 \033[1;93m\$%.4f\033[0m | ⏱️ \033[1;97m%s\033[0m | 📝 \033[1;92m+%s\033[0m/\033[1;91m-%s\033[0m" \ + "$branch" \ + "$model_display" \ + "$cost" \ + "$duration_str" \ + "$lines_added" \ + "$lines_removed" +fi +EOF +chmod +x ~/.claude/scripts/statusline.sh +``` + +4) Print success message: + +- Run: `echo "Installed: ~/.claude/scripts/statusline.sh"` + +5) Configure `~/.claude/settings.json` to enable the status line: + +- First, check if `~/.claude/settings.json` exists. If not, create it with minimal JSON structure: + +```bash +if [ ! -f ~/.claude/settings.json ]; then + echo '{"$schema":"https://json.schemastore.org/claude-code-settings.json"}' > ~/.claude/settings.json +fi +``` + +- Check if `statusLine` is already configured (skip if already set and `--force` is not provided): + +```bash +# Ensure jq is available +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq is required but not installed." >&2 + exit 1 +fi + +# Validate JSON structure before reading fields +if ! jq empty ~/.claude/settings.json >/dev/null 2>&1; then + echo "Error: ~/.claude/settings.json contains invalid JSON. Please fix or remove the file." >&2 + exit 1 +fi + +# Then check for existing statusLine configuration +if jq -e '.statusLine' ~/.claude/settings.json >/dev/null 2>&1 \ + && ! echo "$ARGUMENTS" | grep -q -- '--force'; then + echo "statusLine already configured in ~/.claude/settings.json (use --force to overwrite)" +else + # Add or update statusLine configuration with backup + error handling + cp -p ~/.claude/settings.json ~/.claude/settings.json.backup + tmp_file=$(mktemp ~/.claude/settings.json.tmp.XXXXXX) + trap 'rm -f "$tmp_file"' EXIT + + # Prepare command, optionally including --no-quotes + status_cmd="bash ~/.claude/scripts/statusline.sh" + if echo "$ARGUMENTS" | grep -q -- '--no-quotes'; then + status_cmd="$status_cmd --no-quotes" + fi + + if ! jq --arg cmd "$status_cmd" '.statusLine = {"type": "command", "command": $cmd}' \ + ~/.claude/settings.json > "$tmp_file"; then + echo "Error: Failed to update settings.json. Backup preserved at ~/.claude/settings.json.backup" >&2 + rm -f "$tmp_file" + exit 1 + fi + + # Verify the generated file is not empty and contains valid JSON + if [ ! -s "$tmp_file" ]; then + echo "Error: Generated settings.json is empty. Backup preserved at ~/.claude/settings.json.backup" >&2 + rm -f "$tmp_file" + exit 1 + fi + + if ! jq empty "$tmp_file" >/dev/null 2>&1; then + echo "Error: Generated settings.json contains invalid JSON. Backup preserved at ~/.claude/settings.json.backup" >&2 + rm -f "$tmp_file" + exit 1 + fi + + mv "$tmp_file" ~/.claude/settings.json + trap - EXIT + echo "Configured statusLine in ~/.claude/settings.json (backup at ~/.claude/settings.json.backup)" +fi +``` + +## Notes + +- The script expects JSON input from Claude Code and requires `jq` at runtime. Quotes are fetched via `curl` with a 5-minute cache and a graceful offline fallback. Use `--no-quotes` (or set env `CLAUDE_STATUSLINE_NO_QUOTES=1`) to hide quotes entirely. +- The command automatically updates `~/.claude/settings.json` to enable the status line. Use `--force` to overwrite existing configurations. +- A backup of the previous settings is saved to `~/.claude/settings.json.backup`. Restore with `mv ~/.claude/settings.json.backup ~/.claude/settings.json` if needed. +- To test locally, see the separate "Preview Statusline" command. diff --git a/commands/preview-statusline.md b/commands/preview-statusline.md new file mode 100644 index 0000000..397a611 --- /dev/null +++ b/commands/preview-statusline.md @@ -0,0 +1,54 @@ +--- +description: Preview the status line by piping sample JSON into ~/.claude/scripts/statusline.sh (supports --no-quotes) +allowed-tools: [Bash(test:*), Bash(echo:*), Bash(pwd:*), Bash(cat:*)] +--- + +# Preview Statusline + +Render a sample status line using the installed script and mock JSON. Useful to verify colors and layout. + +## Steps + +1) Ensure the script is installed: + +- Run: `test -x ~/.claude/scripts/statusline.sh || (echo "Not installed. Run: /statusline.install-statusline" && exit 1)` + +2) Build a small sample event JSON and pipe to the script: + +- Run: + +``` +cat <<'JSON' | ~/.claude/scripts/statusline.sh +{ + "cost": { + "total_cost_usd": 0.0123, + "total_duration_ms": 6543, + "total_lines_added": 10, + "total_lines_removed": 2 + }, + "model": { + "display_name": "Sonnet 4.5", + "id": "claude-sonnet-4-5-20250929" + }, + "workspace": { + "project_dir": "$(pwd)" + } +} +JSON +``` + +You should see a single status line printed with colored fields and a quote. + +To preview without the quote section, either add the flag or prefix an env var: + +```bash +# Using flag +cat <<'JSON' | ~/.claude/scripts/statusline.sh --no-quotes +{ "cost": {"total_cost_usd": 0.0123, "total_duration_ms": 6543, "total_lines_added": 10, "total_lines_removed": 2 }, "model": {"display_name": "Sonnet 4.5", "id": "claude-sonnet-4-5-20250929"}, "workspace": {"project_dir": "$(pwd)"} } +JSON + +# Using environment variable +CLAUDE_STATUSLINE_NO_QUOTES=1 bash -lc 'cat <<'\''JSON'\'' | ~/.claude/scripts/statusline.sh' +{ "cost": {"total_cost_usd": 0.0123, "total_duration_ms": 6543, "total_lines_added": 10, "total_lines_removed": 2 }, "model": {"display_name": "Sonnet 4.5", "id": "claude-sonnet-4-5-20250929"}, "workspace": {"project_dir": "$(pwd)"} } +JSON +``` diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..079bc0c --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,49 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:setouchi-h/cc-marketplace:packages/statusline", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "543576c26aa7e5aef5c9020cb47c5a2605a3b534", + "treeHash": "48809b3a4a3f48267eb00223f2a53ffcf8fc0ae16467f41a503acde16a9932ed", + "generatedAt": "2025-11-28T10:28:16.131633Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "statusline", + "description": "Installs a status line script for Claude Code showing branch, model, cost, duration, diff lines, and an optional quote.", + "version": "1.1.1" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "6d9943d4b9393d04d98b9e9beef3ebf61fdd745a6e2acdc4b29566000b7d20d5" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "81b637eecdbde8b4c39969b817960aaff6ff6dc90bd89107a8f0ea692bedb2fb" + }, + { + "path": "commands/preview-statusline.md", + "sha256": "5ec929875c0430069097f29021b9d0978028e83a2f024fec7565271b3c0f891b" + }, + { + "path": "commands/install-statusline.md", + "sha256": "a7e15c341624f2bc8c82de782a55e5d43b0e370e41952f7663aba984d127a32e" + } + ], + "dirSha256": "48809b3a4a3f48267eb00223f2a53ffcf8fc0ae16467f41a503acde16a9932ed" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file