Skip to content

Instantly share code, notes, and snippets.

@y-hirakaw
Last active August 27, 2025 14:31
Show Gist options
  • Select an option

  • Save y-hirakaw/aa59ce04ab5aacabd2752eb1eff47350 to your computer and use it in GitHub Desktop.

Select an option

Save y-hirakaw/aa59ce04ab5aacabd2752eb1eff47350 to your computer and use it in GitHub Desktop.
claude code start lines

/statuslineでまず設定をする

.claude/settings.jsonに以下を追加

  "statusLine": {
    "type": "command",
    "command": "node ~/.claude/statusline.cjs"
  }

~/.claude/statusline.cjs に配置

#!/usr/bin/env node
/**
 * Claude Code Statusline Script
 * 
 * Features:
 * - Token usage tracking with auto-compact percentage
 * - Git diff stats (files changed from parent branch)
 * - Git working directory status (staged, modified, deleted, untracked)
 * - Color-coded indicators for visual feedback
 * - Automatic parent branch detection
 * 
 * Setup Instructions:
 * 1. Place this file at: .claude/workspace/statusline.cjs
 * 2. Make executable: chmod +x .claude/workspace/statusline.cjs
 * 3. Configure in Claude Code settings.json:
 * 
 * "statusline": {
 *   "script": ".claude/workspace/statusline.cjs"
 * }
 * 
 * Optional: Create .claude/workspace/base_branch.yaml:
 * base: main
 * 
 * Display Format:
 * [Model] | 📁 Directory | 🪙 Tokens | Progress% (Remaining) | 📝 Branch Changes | 🔄 Working Changes
 * 
 * Color Codes:
 * - Green: Safe/Good state
 * - Yellow: Warning/Modified
 * - Red: Danger/Deleted
 * - Cyan: Untracked files
 */


const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { execSync } = require('child_process');

// Constants
const COMPACTION_THRESHOLD = 200000 * 0.8; // 80% of 200K tokens

// Read JSON from stdin
let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', async () => {
    try {
        const data = JSON.parse(input);
        
        // Extract basic values from Claude Code JSON structure
        const model = data.model?.display_name || 'Unknown';
        const currentDir = path.basename(data.workspace?.current_dir || data.cwd || '.');
        const sessionId = data.session_id;
        const transcriptPath = data.transcript_path;
        
        // 1. Calculate token usage for current session
        let totalTokens = 0;
        
        if (sessionId) {
            // First try direct transcript path if provided
            if (transcriptPath && fs.existsSync(transcriptPath)) {
                totalTokens = await calculateTokensFromTranscript(transcriptPath);
            } else {
                // Try multiple locations for transcript files
                const transcriptLocations = [
                    path.join(process.env.HOME, '.claude', 'projects'),
                    path.join(process.env.HOME, '.claude', 'sessions'),
                    path.join(process.cwd(), '.claude', 'sessions')
                ];
            
            for (const location of transcriptLocations) {
                if (fs.existsSync(location)) {
                    // Look for session files in subdirectories
                    try {
                        const entries = fs.readdirSync(location, { withFileTypes: true });
                        
                        for (const entry of entries) {
                            if (entry.isDirectory()) {
                                const transcriptFile = path.join(location, entry.name, `${sessionId}.jsonl`);
                                if (fs.existsSync(transcriptFile)) {
                                    totalTokens = await calculateTokensFromTranscript(transcriptFile);
                                    break;
                                }
                            } else if (entry.name === `${sessionId}.jsonl`) {
                                // Direct session file
                                const transcriptFile = path.join(location, entry.name);
                                totalTokens = await calculateTokensFromTranscript(transcriptFile);
                                break;
                            }
                        }
                        
                        if (totalTokens > 0) break; // Found tokens, stop searching
                    } catch (error) {
                        // Continue to next location on error
                    }
                }
            }
            }
        }
        
        // Add default 5K buffer to account for discrepancy with Claude Code
        totalTokens += 5000;
        
        // 2. Calculate auto-compact percentage
        const percentage = Math.min(100, Math.round((totalTokens / COMPACTION_THRESHOLD) * 100));
        const percentageColor = getPercentageColor(percentage);
        
        // 3. Get git diff stats (files changed)
        const gitDiffStats = getGitDiffStats();
        const gitStatus = getGitStatus();
        
        // Format token display
        const tokenDisplay = formatTokenCount(totalTokens);
        
        // Calculate remaining tokens before auto-compact
        const remainingTokens = Math.max(0, COMPACTION_THRESHOLD - totalTokens);
        const remainingDisplay = formatTokenCount(remainingTokens);
        
        // Build status line
        const statusLine = [
            `[${model}]`,
            `📁 ${currentDir}`,
            `🪙 ${tokenDisplay}`,
            `${percentageColor}${percentage}%\x1b[0m (${remainingDisplay} left)`,
            gitDiffStats,
            gitStatus
        ].filter(Boolean).join(' | ');
        
        console.log(statusLine);
        
    } catch (error) {
        // Fallback status line on error
        console.log(`[Error] 📁 . | 🪙 0 | 0% | ${error.message}`);
    }
});

async function calculateTokensFromTranscript(filePath) {
    return new Promise((resolve, reject) => {
        let totalTokens = 0;
        let lastUsage = null;
        
        // Check if file exists and is readable
        if (!fs.existsSync(filePath)) {
            resolve(0);
            return;
        }
        
        const fileStream = fs.createReadStream(filePath);
        const rl = readline.createInterface({
            input: fileStream,
            crlfDelay: Infinity
        });
        
        rl.on('line', (line) => {
            try {
                const entry = JSON.parse(line);
                
                // Get the latest cumulative usage data (most accurate)
                if (entry.type === 'assistant' && entry.message?.usage) {
                    lastUsage = entry.message.usage;
                }
                
                // Also check for turn-level usage (alternative format)
                if (entry.usage) {
                    lastUsage = entry.usage;
                }
            } catch (e) {
                // Skip invalid JSON lines silently
            }
        });
        
        rl.on('close', () => {
            if (lastUsage) {
                // Calculate total tokens including all types
                totalTokens = (lastUsage.input_tokens || 0) +
                              (lastUsage.output_tokens || 0) +
                              (lastUsage.cache_creation_input_tokens || 0) +
                              (lastUsage.cache_read_input_tokens || 0);
                
                // Handle alternative usage format
                if (totalTokens === 0 && lastUsage.total_tokens) {
                    totalTokens = lastUsage.total_tokens;
                }
            }
            resolve(totalTokens);
        });
        
        rl.on('error', (err) => {
            // Don't reject on file read errors, just return 0
            resolve(0);
        });
    });
}

function getParentBranch() {
    try {
        // Method 1: Check .claude/workspace/base_branch.yaml (highest priority)
        const claudeConfigFile = path.join(process.cwd(), '.claude', 'workspace', 'base_branch.yaml');
        if (fs.existsSync(claudeConfigFile)) {
            try {
                const content = fs.readFileSync(claudeConfigFile, 'utf8');
                const match = content.match(/^base:\s*(.+)$/m);
                if (match) {
                    return match[1].trim();
                }
            } catch {}
        }
        
        // Method 2: Check for stored parent branch in git config
        try {
            const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
            const parentBranch = execSync(`git config --get branch.${currentBranch}.parent`, { encoding: 'utf8' }).trim();
            if (parentBranch) return parentBranch;
        } catch {}
        
        // Method 3: Check for a .git/parent-branch file (custom convention)
        const parentBranchFile = path.join(process.cwd(), '.git', 'parent-branch');
        if (fs.existsSync(parentBranchFile)) {
            const parentBranch = fs.readFileSync(parentBranchFile, 'utf8').trim();
            if (parentBranch) return parentBranch;
        }
        
        // Method 3: Try to find from reflog
        try {
            const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
            const reflog = execSync(`git reflog show ${currentBranch} --format="%gs" 2>/dev/null | grep "branch: Created from" | head -1`, { encoding: 'utf8' }).trim();
            const match = reflog.match(/branch: Created from (.+)/);
            if (match) {
                // Clean up the branch name (remove origin/ prefix if present)
                return match[1].replace('origin/', '');
            }
        } catch {}
        
        // Method 4: Find the closest branch that contains our oldest unique commit
        try {
            const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
            
            // Get all local branches except current
            const branches = execSync('git branch --format="%(refname:short)"', { encoding: 'utf8' })
                .trim()
                .split('\n')
                .filter(b => b && b !== currentBranch);
            
            // Find merge-base with each branch and pick the most recent one
            let bestBranch = null;
            let bestCommitDate = 0;
            
            for (const branch of branches) {
                try {
                    const mergeBase = execSync(`git merge-base ${branch} HEAD 2>/dev/null`, { encoding: 'utf8' }).trim();
                    if (mergeBase) {
                        const commitDate = parseInt(execSync(`git show -s --format=%ct ${mergeBase}`, { encoding: 'utf8' }).trim());
                        if (commitDate > bestCommitDate) {
                            bestCommitDate = commitDate;
                            bestBranch = branch;
                        }
                    }
                } catch {}
            }
            
            if (bestBranch) return bestBranch;
        } catch {}
        
        // Fallback: Try main or master
        try {
            execSync('git rev-parse --verify main', { stdio: 'ignore' });
            return 'main';
        } catch {
            try {
                execSync('git rev-parse --verify master', { stdio: 'ignore' });
                return 'master';
            } catch {
                return null;
            }
        }
    } catch {
        return null;
    }
}

function getGitDiffStats() {
    try {
        // First check if we're in a git repository
        execSync('git rev-parse --git-dir', { stdio: 'ignore' });
        
        // Get current branch
        const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
        if (!currentBranch) {
            return null; // Detached HEAD state
        }
        
        // Get the parent/base branch
        const parentBranch = getParentBranch();
        if (!parentBranch) {
            return '📝 No parent';
        }
        
        // If we're on the parent branch itself, show working directory changes
        if (currentBranch === parentBranch) {
            try {
                // Get number of modified/staged files in working directory
                const statusOutput = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
                if (!statusOutput) {
                    return '📝 0 files';
                }
                
                // Count files, expanding untracked directories
                let fileCount = 0;
                const lines = statusOutput.split('\n');
                
                for (const line of lines) {
                    const status = line.substring(0, 2);
                    const filePath = line.substring(3);
                    
                    // If it's an untracked directory, count individual files in it
                    if (status === '??') {
                        try {
                            // Check if it's a directory
                            const fullPath = path.join(process.cwd(), filePath);
                            const stat = fs.statSync(fullPath);
                            if (stat.isDirectory()) {
                                // Count files recursively in the directory
                                const findOutput = execSync(`find "${fullPath}" -type f | wc -l`, { encoding: 'utf8' }).trim();
                                fileCount += parseInt(findOutput) || 1;
                            } else {
                                fileCount++;
                            }
                        } catch {
                            fileCount++; // Fallback: count as single file
                        }
                    } else {
                        fileCount++;
                    }
                }
                let color = '\x1b[32m'; // Green for small changes
                if (fileCount >= 10) color = '\x1b[33m'; // Yellow for medium
                if (fileCount >= 25) color = '\x1b[31m'; // Red for large
                
                const fileText = fileCount === 1 ? 'file' : 'files';
                return `📝 ${color}${fileCount} ${fileText}\x1b[0m (working dir)`;
            } catch {
                return '📝 0 files';
            }
        }
        
        try {
            // Make sure the parent branch exists
            execSync(`git rev-parse --verify ${parentBranch}`, { stdio: 'ignore' });
            
            // Get the number of changed files
            const changedFiles = execSync(`git diff ${parentBranch}...HEAD --name-only 2>/dev/null | wc -l`, { encoding: 'utf8' }).trim();
            const fileCount = parseInt(changedFiles) || 0;
            
            // Get additional statistics for context
            const diffStat = execSync(`git diff ${parentBranch}...HEAD --shortstat 2>/dev/null || echo ""`, { encoding: 'utf8' }).trim();
            
            // Color code based on number of files changed
            let color = '\x1b[32m'; // Green for small changes (< 10 files)
            if (fileCount >= 10) color = '\x1b[33m'; // Yellow for medium (10-25 files)
            if (fileCount >= 25) color = '\x1b[31m'; // Red for large (25+ files)
            
            // Format the display
            const fileText = fileCount === 1 ? 'file' : 'files';
            
            // Show parent branch name for clarity
            const parentDisplay = parentBranch.length > 20 
                ? '...' + parentBranch.slice(-17) 
                : parentBranch;
            
            return `📝 ${color}${fileCount} ${fileText}\x1b[0m (← ${parentDisplay})`;
            
        } catch (error) {
            // Parent branch doesn't exist or other git error
            return `📝 ? files (← ${parentBranch})`;
        }
        
    } catch (error) {
        // Not in a git repo or other error
        return null;
    }
}

function getGitStatus() {
    try {
        // First check if we're in a git repository
        execSync('git rev-parse --git-dir', { stdio: 'ignore' });
        
        // Get git status --porcelain output
        const statusOutput = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
        
        if (!statusOutput) {
            return '✨ Clean'; // Working directory is clean
        }
        
        const lines = statusOutput.split('\n');
        let modified = 0;
        let added = 0;
        let deleted = 0;
        let untracked = 0;
        let staged = 0;
        
        for (const line of lines) {
            const statusCode = line.substring(0, 2);
            const x = statusCode[0]; // staged status
            const y = statusCode[1]; // working tree status
            
            // Count staged changes (ignore untracked files)
            if (x !== ' ' && statusCode !== '??') staged++;
            
            // Count working tree changes
            switch (y) {
                case 'M': modified++; break;
                case 'D': deleted++; break;
                case 'A': added++; break;
                case '?': untracked++; break;
            }
        }
        
        // Build status display
        const parts = [];
        if (staged > 0) parts.push(`\x1b[32m+${staged}\x1b[0m`); // Green for staged
        if (modified > 0) parts.push(`\x1b[33m~${modified}\x1b[0m`); // Yellow for modified
        if (added > 0) parts.push(`\x1b[32m+${added}\x1b[0m`); // Green for added
        if (deleted > 0) parts.push(`\x1b[31m-${deleted}\x1b[0m`); // Red for deleted
        if (untracked > 0) parts.push(`\x1b[36m?${untracked}\x1b[0m`); // Cyan for untracked
        
        if (parts.length === 0) {
            return '✨ Clean';
        }
        
        return `🔄 ${parts.join(' ')}`;
        
    } catch (error) {
        // Not in a git repo or other error
        return null;
    }
}
function getPercentageColor(percentage) {
    if (percentage >= 90) return '\x1b[31m'; // Red - danger zone
    if (percentage >= 70) return '\x1b[33m'; // Yellow - warning
    return '\x1b[32m'; // Green - safe
}

function formatTokenCount(tokens) {
    if (tokens >= 1000000) {
        return `${(tokens / 1000000).toFixed(1)}M`;
    } else if (tokens >= 1000) {
        // Use more precise display for K values
        const kValue = tokens / 1000;
        return kValue >= 10 ? `${Math.round(kValue)}K` : `${kValue.toFixed(1)}K`;
    }
    return tokens.toString();
}

function getSessionAge() {
    try {
        // Get the age of the current session for context
        const gitLogOutput = execSync('git log --oneline -1 --format="%cr"', { 
            encoding: 'utf8', 
            stdio: ['ignore', 'pipe', 'ignore'] 
        }).trim();
        
        // Simplify relative time display
        return gitLogOutput.replace(/ ago$/, '').replace(/^\d+ /, '');
    } catch {
        return null;
    }
}

.claude/workspace/base_branch.yaml マージ先のブランチ名を入れる

base: mainを記載 /**

@y-hirakaw
Copy link
Author

@{ファイル名}がスラッシュコマンドなどで出来るようになっているのでそれも試したい

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment