Initial commit
This commit is contained in:
13
.claude-plugin/plugin.json
Normal file
13
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# statusline
|
||||
|
||||
Installs a status line script for Claude Code showing branch, model, cost, duration, diff lines, and an optional quote.
|
||||
300
commands/install-statusline.md
Normal file
300
commands/install-statusline.md
Normal file
@@ -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.
|
||||
54
commands/preview-statusline.md
Normal file
54
commands/preview-statusline.md
Normal file
@@ -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
|
||||
```
|
||||
49
plugin.lock.json
Normal file
49
plugin.lock.json
Normal file
@@ -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": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user