|
package main |
|
|
|
import ( |
|
"encoding/json" |
|
"flag" |
|
"fmt" |
|
"io" |
|
"os" |
|
"os/exec" |
|
"path/filepath" |
|
"strings" |
|
) |
|
|
|
type Input struct { |
|
Workspace struct { |
|
CurrentDir string `json:"current_dir"` |
|
} `json:"workspace"` |
|
CWD string `json:"cwd"` |
|
Model struct { |
|
DisplayName string `json:"display_name"` |
|
ID string `json:"id"` |
|
} `json:"model"` |
|
SessionID string `json:"session_id"` |
|
TranscriptPath string `json:"transcript_path"` |
|
OutputStyle struct { |
|
Name string `json:"name"` |
|
} `json:"output_style"` |
|
} |
|
|
|
type Block struct { |
|
IsActive bool `json:"isActive"` |
|
CostUSD float64 `json:"costUSD"` |
|
BurnRate struct { |
|
CostPerHour float64 `json:"costPerHour"` |
|
} `json:"burnRate"` |
|
Projection struct { |
|
RemainingMinutes int `json:"remainingMinutes"` |
|
} `json:"projection"` |
|
} |
|
|
|
type BlocksResponse struct { |
|
Blocks []Block `json:"blocks"` |
|
} |
|
|
|
type Session struct { |
|
SessionID string `json:"sessionId"` |
|
TotalCost float64 `json:"totalCost"` |
|
InputTokens int `json:"inputTokens"` |
|
OutputTokens int `json:"outputTokens"` |
|
LastActivity string `json:"lastActivity"` |
|
} |
|
|
|
type SessionsResponse struct { |
|
Sessions []Session `json:"sessions"` |
|
} |
|
|
|
func main() { |
|
// Parse command line flags |
|
mode := flag.String("mode", "pro", "Mode: 'pro' for Pro plan with blocks, 'api' for API usage") |
|
flag.Parse() |
|
|
|
input, err := io.ReadAll(os.Stdin) |
|
if err != nil { |
|
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) |
|
os.Exit(1) |
|
} |
|
|
|
var data Input |
|
if err := json.Unmarshal(input, &data); err != nil { |
|
fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err) |
|
os.Exit(1) |
|
} |
|
|
|
// Extract data |
|
currentDir := data.Workspace.CurrentDir |
|
if currentDir == "" { |
|
currentDir = data.CWD |
|
} |
|
|
|
modelName := data.Model.DisplayName |
|
if modelName == "" { |
|
modelName = data.Model.ID |
|
} |
|
if modelName == "" { |
|
modelName = "Claude" |
|
} |
|
|
|
outputStyle := data.OutputStyle.Name |
|
|
|
// Get directory name and git info |
|
dirName := filepath.Base(currentDir) |
|
gitInfo := getGitInfo(currentDir) |
|
|
|
// Get metrics based on mode |
|
var metricsParts []string |
|
if *mode == "api" { |
|
sessionCost, requestCount := getSessionMetrics() |
|
if sessionCost != "" { |
|
metricsParts = append(metricsParts, fmt.Sprintf("\033[32mSession: %s\033[0m", sessionCost)) |
|
} |
|
if requestCount != "" { |
|
metricsParts = append(metricsParts, fmt.Sprintf("\033[36m%s requests\033[0m", requestCount)) |
|
} |
|
} else { |
|
blockCost, burnRate, timeRemaining := getBlockMetrics() |
|
if blockCost != "" { |
|
metricsParts = append(metricsParts, fmt.Sprintf("\033[32m%s\033[0m", blockCost)) |
|
} |
|
if burnRate != "" { |
|
metricsParts = append(metricsParts, fmt.Sprintf("%s", burnRate)) |
|
} |
|
if timeRemaining != "" { |
|
metricsParts = append(metricsParts, fmt.Sprintf("\033[36m%s\033[2m left\033[0m", timeRemaining)) |
|
} |
|
} |
|
|
|
// Build status line |
|
if gitInfo != "" { |
|
fmt.Printf("\033[2m\033[94m%s\033[0m %s ", dirName, gitInfo) |
|
} else { |
|
fmt.Printf("\033[2m\033[94m%s\033[0m ", dirName) |
|
} |
|
|
|
fmt.Printf("\033[2mvia\033[0m \033[35m%s\033[0m", modelName) |
|
|
|
if outputStyle != "" && outputStyle != "default" { |
|
fmt.Printf(" \033[2min\033[0m \033[33m%s\033[0m", outputStyle) |
|
} |
|
|
|
// Display metrics |
|
for _, part := range metricsParts { |
|
fmt.Printf(" \033[2m│\033[0m %s", part) |
|
} |
|
} |
|
|
|
func getGitInfo(currentDir string) string { |
|
gitDir := filepath.Join(currentDir, ".git") |
|
if _, err := os.Stat(gitDir); os.IsNotExist(err) { |
|
return "" |
|
} |
|
|
|
cmd := exec.Command("git", "branch", "--show-current") |
|
cmd.Dir = currentDir |
|
output, err := cmd.Output() |
|
if err != nil { |
|
return "" |
|
} |
|
|
|
branch := strings.TrimSpace(string(output)) |
|
if branch == "" { |
|
return "" |
|
} |
|
|
|
// Check for uncommitted changes |
|
diffCmd := exec.Command("git", "diff", "--quiet") |
|
diffCmd.Dir = currentDir |
|
diffErr := diffCmd.Run() |
|
|
|
cachedCmd := exec.Command("git", "diff", "--cached", "--quiet") |
|
cachedCmd.Dir = currentDir |
|
cachedErr := cachedCmd.Run() |
|
|
|
if diffErr != nil || cachedErr != nil { |
|
return fmt.Sprintf("on \033[91m%s\033[2m*\033[0m", branch) |
|
} |
|
return fmt.Sprintf("on \033[92m%s\033[0m", branch) |
|
} |
|
|
|
func getBlockMetrics() (blockCost, burnRate, timeRemaining string) { |
|
cmd := exec.Command("ccusage", "blocks", "--active", "--json") |
|
output, err := cmd.Output() |
|
if err != nil { |
|
return "<1¢", "", "" |
|
} |
|
|
|
var response BlocksResponse |
|
if err := json.Unmarshal(output, &response); err != nil { |
|
return "<1¢", "", "" |
|
} |
|
|
|
var activeBlock *Block |
|
for _, block := range response.Blocks { |
|
if block.IsActive { |
|
activeBlock = &block |
|
break |
|
} |
|
} |
|
|
|
if activeBlock == nil { |
|
return "<1¢", "", "" |
|
} |
|
|
|
// Format block cost |
|
if activeBlock.CostUSD > 0 { |
|
costCents := int(activeBlock.CostUSD * 100) |
|
if costCents < 100 { |
|
blockCost = fmt.Sprintf("%d¢", costCents) |
|
} else { |
|
dollars := costCents / 100 |
|
cents := costCents % 100 |
|
if cents == 0 { |
|
blockCost = fmt.Sprintf("$%d", dollars) |
|
} else { |
|
blockCost = fmt.Sprintf("$%d.%02d", dollars, cents) |
|
} |
|
} |
|
} else { |
|
blockCost = "<1¢" |
|
} |
|
|
|
// Format burn rate with quota-aware indicators |
|
if activeBlock.BurnRate.CostPerHour > 0 { |
|
burnCents := int(activeBlock.BurnRate.CostPerHour * 100) |
|
|
|
// Calculate quota intensity (assuming $100 Pro plan, 30-day blocks) |
|
// Sustainable rate: $100 / (30 days * 24 hours) ≈ $0.139/hr |
|
sustainableRatePerHour := 100.0 / (30.0 * 24.0) // $0.139/hr |
|
intensity := activeBlock.BurnRate.CostPerHour / sustainableRatePerHour |
|
|
|
var emoji string |
|
if intensity <= 0.25 { |
|
emoji = "🟢" // Low burn (0-25%) |
|
} else if intensity <= 0.50 { |
|
emoji = "🟡" // Moderate burn (25-50%) |
|
} else if intensity <= 0.75 { |
|
emoji = "🟠" // High burn (50-75%) |
|
} else if intensity <= 1.0 { |
|
emoji = "🔥" // Critical burn (75-100%) |
|
} else { |
|
emoji = "🚨" // Over-quota burn (>100%) |
|
} |
|
|
|
if burnCents < 100 { |
|
burnRate = fmt.Sprintf("\033[33m%s %d¢/hr\033[0m", emoji, burnCents) |
|
} else { |
|
dollars := burnCents / 100 |
|
cents := burnCents % 100 |
|
if cents == 0 { |
|
burnRate = fmt.Sprintf("\033[33m%s $%d/hr\033[0m", emoji, dollars) |
|
} else { |
|
burnRate = fmt.Sprintf("\033[33m%s $%d.%02d/hr\033[0m", emoji, dollars, cents) |
|
} |
|
} |
|
} |
|
|
|
// Format time remaining |
|
if activeBlock.Projection.RemainingMinutes > 0 { |
|
hours := activeBlock.Projection.RemainingMinutes / 60 |
|
minutes := activeBlock.Projection.RemainingMinutes % 60 |
|
if hours > 0 { |
|
timeRemaining = fmt.Sprintf("%dh%dm", hours, minutes) |
|
} else { |
|
timeRemaining = fmt.Sprintf("%dm", minutes) |
|
} |
|
} |
|
|
|
return blockCost, burnRate, timeRemaining |
|
} |
|
|
|
func getSessionMetrics() (sessionCost, requestCount string) { |
|
cmd := exec.Command("ccusage", "session", "--json") |
|
output, err := cmd.Output() |
|
if err != nil { |
|
return "<1¢", "" |
|
} |
|
|
|
var response SessionsResponse |
|
if err := json.Unmarshal(output, &response); err != nil { |
|
return "<1¢", "" |
|
} |
|
|
|
// Find the most recent session (assuming they're sorted by last activity) |
|
var currentSession *Session |
|
if len(response.Sessions) > 0 { |
|
// Use the first session (most recent if sorted) |
|
currentSession = &response.Sessions[0] |
|
} |
|
|
|
if currentSession == nil { |
|
return "<1¢", "" |
|
} |
|
|
|
// Format session cost |
|
if currentSession.TotalCost > 0 { |
|
costCents := int(currentSession.TotalCost * 100) |
|
if costCents < 100 { |
|
sessionCost = fmt.Sprintf("%d¢", costCents) |
|
} else { |
|
dollars := costCents / 100 |
|
cents := costCents % 100 |
|
if cents == 0 { |
|
sessionCost = fmt.Sprintf("$%d", dollars) |
|
} else { |
|
sessionCost = fmt.Sprintf("$%d.%02d", dollars, cents) |
|
} |
|
} |
|
} else { |
|
sessionCost = "<1¢" |
|
} |
|
|
|
// Format request count using tokens as proxy (input + output tokens) |
|
totalTokens := currentSession.InputTokens + currentSession.OutputTokens |
|
if totalTokens > 0 { |
|
// Convert tokens to approximate "requests" - rough estimate |
|
requestCount = fmt.Sprintf("~%d", (totalTokens+1999)/2000) // Assume ~2k tokens per request |
|
} |
|
|
|
return sessionCost, requestCount |
|
} |