From 96f148f0810fd11c3197ea1b241056c13bcf1c05 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Tue, 20 Jan 2026 22:56:15 +0000 Subject: [PATCH] feat(claude): add Claude Code settings and statusline configuration Add global Claude Code configuration with permission allowlists for common build/test commands, statusline script showing git status, context usage, token counts, and cost. Update siren to symlink these files to ~/.claude/ directory. Co-Authored-By: Claude Opus 4.5 --- claude/settings.json | 59 +++++++++++ claude/statusline.sh | 245 +++++++++++++++++++++++++++++++++++++++++++ siren | 2 + 3 files changed, 306 insertions(+) create mode 100644 claude/settings.json create mode 100755 claude/statusline.sh diff --git a/claude/settings.json b/claude/settings.json new file mode 100644 index 0000000..c34d1fc --- /dev/null +++ b/claude/settings.json @@ -0,0 +1,59 @@ +{ + "permissions": { + "allow": [ + "Bash(bundle check:*)", + "Bash(bundle install:*)", + "Bash(bundle list:*)", + "Bash(cargo build:*)", + "Bash(cargo clippy:*)", + "Bash(cargo fmt:*)", + "Bash(cargo test:*)", + "Bash(git add:*)", + "Bash(go build:*)", + "Bash(go get:*)", + "Bash(go mod init:*)", + "Bash(go mod tidy:*)", + "Bash(go test:*)", + "Bash(golangci-lint run:*)", + "Bash(pnpm build:*)", + "Bash(pnpm install:*)", + "Bash(pnpm lint:*)", + "Bash(pnpm test:*)", + "Bash(pnpm typecheck:*)", + "mcp__deepwiki__ask_question", + "mcp__deepwiki__read_wiki_contents", + "mcp__deepwiki__read_wiki_structure" + ], + "deny": [ + "Bash(curl:*)", + "Read(./.env)", + "Read(./.env*)", + "Read(./secrets/**)" + ] + }, + "plansDirectory": ".claude/plans", + "model": "opus", + "statusLine": { + "type": "command", + "command": "~/.claude/statusline.sh", + "padding": 0 + }, + "enabledPlugins": { + "typescript-lsp@claude-plugins-official": true, + "gopls-lsp@claude-plugins-official": true, + "feature-dev@claude-plugins-official": true, + "code-review@claude-plugins-official": true, + "commit-commands@claude-plugins-official": true, + "plugin-dev@claude-plugins-official": true, + "frontend-design@claude-plugins-official": true, + "playwright@claude-plugins-official": true, + "security-guidance@claude-plugins-official": true, + "code-simplifier@claude-plugins-official": true, + "ralph-loop@claude-plugins-official": true, + "pr-review-toolkit@claude-plugins-official": true, + "rust-analyzer-lsp@claude-plugins-official": true, + "swift-lsp@claude-plugins-official": true, + "lua-lsp@claude-plugins-official": true + }, + "autoUpdatesChannel": "latest" +} diff --git a/claude/statusline.sh b/claude/statusline.sh new file mode 100755 index 0000000..44fb371 --- /dev/null +++ b/claude/statusline.sh @@ -0,0 +1,245 @@ +#!/bin/bash + +# --- Color Constants --- +COLOR_DIR=245 # directory +COLOR_GIT_BRANCH=153 # light blue pastel +COLOR_GIT_STATUS=182 # pink pastel +COLOR_DIM=243 # dimmer text (lines, cost) +COLOR_SEP=242 # separators + +# --- Utility Functions --- + +# Print text in specified 256-color +colored() { + printf "\033[38;5;%sm%s\033[0m" "$1" "$2" +} + +# Print separator +sep() { + colored $COLOR_SEP " · " +} + +# Format token counts (e.g., 50k, 1.2M) +format_tokens() { + local tokens=$1 + if [ "$tokens" -ge 1000000 ]; then + awk "BEGIN {printf \"%.1fM\", $tokens/1000000}" + elif [ "$tokens" -ge 1000 ]; then + awk "BEGIN {printf \"%.0fk\", $tokens/1000}" + else + echo "$tokens" + fi +} + +# Return color code based on percentage threshold +# Args: $1 = percentage, $2 = base color (used when below warning threshold) +get_percentage_color() { + local percent=$1 + local base_color=$2 + + # 229 = light yellow, 221 = yellow, 214 = gold, 208 = orange + if [ "$percent" -ge 98 ]; then + echo 208 + elif [ "$percent" -ge 95 ]; then + echo 214 + elif [ "$percent" -ge 90 ]; then + echo 221 + elif [ "$percent" -ge 85 ]; then + echo 229 + else + echo "$base_color" + fi +} + +# --- Data Extraction --- + +# Read stdin, save JSON, extract all fields into globals +parse_input() { + INPUT=$(cat) + + MODEL=$(echo "$INPUT" | jq -r '.model.display_name') + CWD=$(echo "$INPUT" | jq -r '.workspace.current_dir') + PERCENT=$(echo "$INPUT" | jq -r '.context_window.used_percentage // 0' | + xargs printf "%.0f") + TOTAL_INPUT=$(echo "$INPUT" | jq -r '.context_window.total_input_tokens // 0') + TOTAL_OUTPUT=$(echo "$INPUT" | jq -r '.context_window.total_output_tokens // 0') + TOTAL_TOKENS=$((TOTAL_INPUT + TOTAL_OUTPUT)) + CONTEXT_SIZE=$(echo "$INPUT" | jq -r '.context_window.context_window_size // 0') + # Calculate currently loaded tokens from percentage + CURRENT_TOKENS=$((CONTEXT_SIZE * PERCENT / 100)) + + # Extract cost info + COST_USD=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0') + LINES_ADDED=$(echo "$INPUT" | jq -r '.cost.total_lines_added // 0') + LINES_REMOVED=$(echo "$INPUT" | jq -r '.cost.total_lines_removed // 0') +} + +# --- Component Builders --- + +# Get CWD, replace $HOME with ~ +get_directory() { + if [ -n "$CWD" ]; then + DIR="$CWD" + else + DIR=$(pwd) + fi + + # Replace home directory with tilde + DIR="${DIR/#$HOME/~}" +} + +# Get branch, status indicators, ahead/behind +get_git_info() { + GIT_BRANCH="" + GIT_STATUS="" + GIT_AHEAD_BEHIND="" + + # Skip if not in a git repo (skip optional locks to avoid blocking) + if [ ! -d "${CWD:-.}/.git" ] && + ! git -C "${CWD:-.}" rev-parse --git-dir > /dev/null 2>&1; then + return + fi + + # Get branch name + GIT_BRANCH=$(git -C "${CWD:-.}" branch --show-current 2> /dev/null || + git -C "${CWD:-.}" rev-parse --short HEAD 2> /dev/null) + + [ -z "$GIT_BRANCH" ] && return + + # Get status indicators + local git_dirty="" git_staged="" git_untracked="" + + # Check for staged changes + if ! git -C "${CWD:-.}" diff --cached --quiet 2> /dev/null; then + git_staged="+" + fi + + # Check for unstaged changes + if ! git -C "${CWD:-.}" diff --quiet 2> /dev/null; then + git_dirty="!" + fi + + # Check for untracked files + if [ -n "$(git -C "${CWD:-.}" ls-files --others --exclude-standard 2> /dev/null)" ]; then + git_untracked="?" + fi + + # Combine status indicators + GIT_STATUS="${git_staged}${git_dirty}${git_untracked}" + + # Get ahead/behind counts + local upstream ahead behind + upstream=$(git -C "${CWD:-.}" rev-parse --abbrev-ref '@{upstream}' 2> /dev/null) + if [ -n "$upstream" ]; then + ahead=$(git -C "${CWD:-.}" rev-list --count '@{upstream}..HEAD' 2> /dev/null) + behind=$(git -C "${CWD:-.}" rev-list --count 'HEAD..@{upstream}' 2> /dev/null) + + if [ "$ahead" -gt 0 ]; then + GIT_AHEAD_BEHIND="↑${ahead}" + fi + if [ "$behind" -gt 0 ]; then + GIT_AHEAD_BEHIND="${GIT_AHEAD_BEHIND}↓${behind}" + fi + fi +} + +# Build braille progress bar from PERCENT +build_progress_bar() { + # Braille characters with 7 levels per cell + # ⣀ (2) -> ⣄ (3) -> ⣤ (4) -> ⣦ (5) -> ⣶ (6) -> ⣷ (7) -> ⣿ (8 dots) + local braille_chars=("⣀" "⣄" "⣤" "⣦" "⣶" "⣷" "⣿") + local bar_width=10 + local levels=7 + local total_gradations=$((bar_width * levels)) + local current_gradation=$((PERCENT * total_gradations / 100)) + + PROGRESS_BAR="" + for ((i = 0; i < bar_width; i++)); do + local cell_start=$((i * levels)) + local cell_fill=$((current_gradation - cell_start)) + + if [ $cell_fill -le 0 ]; then + # Empty cell + PROGRESS_BAR+="${braille_chars[0]}" + elif [ $cell_fill -ge $levels ]; then + # Full cell + PROGRESS_BAR+="${braille_chars[$((levels - 1))]}" + else + # Partial cell + PROGRESS_BAR+="${braille_chars[$cell_fill]}" + fi + done +} + +# --- Output --- + +# Print the final formatted statusline +print_statusline() { + local current_display total_display cost_display context_color + + current_display=$(format_tokens "$CURRENT_TOKENS") + total_display=$(format_tokens "$TOTAL_TOKENS") + + # Determine context color based on percentage (ramps to warning colors) + context_color=$(get_percentage_color "$PERCENT" $COLOR_DIM) + + # Format cost as $X.XX + cost_display=$(awk "BEGIN {printf \"$%.2f\", $COST_USD}") + + # Directory + colored $COLOR_DIR "$DIR" + + # Git info + if [ -n "$GIT_BRANCH" ]; then + printf " " + colored $COLOR_GIT_BRANCH "$GIT_BRANCH" + + # Status indicators + if [ -n "$GIT_STATUS" ]; then + colored $COLOR_GIT_STATUS "$GIT_STATUS" + fi + + # Ahead/behind + if [ -n "$GIT_AHEAD_BEHIND" ]; then + printf " " + colored $COLOR_GIT_STATUS "$GIT_AHEAD_BEHIND" + fi + fi + + sep + + # Model (only if not default Opus 4.5) + if [ "$MODEL" != "Opus 4.5" ]; then + colored $COLOR_DIR "$MODEL" + sep + fi + + # Lines added/removed + colored $COLOR_DIM "+$LINES_ADDED" + colored $COLOR_SEP "/" + colored $COLOR_DIM "-$LINES_REMOVED" + sep + + # Progress bar and percentage (dynamic color based on context usage) + colored "$context_color" "$PROGRESS_BAR $PERCENT%" + sep + + # Token counts (dynamic color based on context usage) + colored "$context_color" "$current_display/$total_display" + sep + + # Cost + colored $COLOR_DIM "$cost_display" +} + +# --- Entry Point --- + +main() { + parse_input + get_directory + get_git_info + build_progress_bar + print_statusline +} + +main "$@" diff --git a/siren b/siren index a22a1ff..0e90b68 100755 --- a/siren +++ b/siren @@ -39,6 +39,8 @@ define_settings() { # Additional static symlinks to create (source => target). STATIC_SYMLINKS["claude/CLAUDE.md"]="${HOME}/.claude/CLAUDE.md" + STATIC_SYMLINKS["claude/settings.json"]="${HOME}/.claude/settings.json" + STATIC_SYMLINKS["claude/statusline.sh"]="${HOME}/.claude/statusline.sh" STATIC_SYMLINKS["cspell/vscode-user-dictionary.txt"]="${HOME}/.cspell/vscode-user-dictionary.txt" STATIC_SYMLINKS["harper-ls/dictionary.txt"]="$(harper_config_dir)/dictionary.txt" STATIC_SYMLINKS["harper-ls/file_dictionaries"]="$(harper_config_dir)/file_dictionaries"