/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を記載
/**
@{ファイル名}がスラッシュコマンドなどで出来るようになっているのでそれも試したい