Skip to content

Commit 0cd5c4a

Browse files
AnnatarHeclaude
andcommitted
feat(cc): add statusline command for Claude Code integration
Add `shelltime cc statusline` command that outputs a formatted status line for Claude Code, displaying model name, session cost, daily cost from API, and context window usage percentage. Features: - Reads JSON from stdin (passed by Claude Code) - Fetches daily cost from GraphQL API with 5-minute cache - Color-coded context percentage (green/yellow/red) - 100ms hard timeout for fast statusline updates - Graceful error handling with fallback output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0737e79 commit 0cd5c4a

6 files changed

Lines changed: 543 additions & 0 deletions

File tree

commands/cc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var CCCommand = &cli.Command{
1212
Subcommands: []*cli.Command{
1313
CCInstallCommand,
1414
CCUninstallCommand,
15+
CCStatuslineCommand,
1516
},
1617
}
1718

commands/cc_statusline.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package commands
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"os"
10+
"strings"
11+
"time"
12+
13+
"github.com/gookit/color"
14+
"github.com/malamtime/cli/model"
15+
"github.com/urfave/cli/v2"
16+
)
17+
18+
var CCStatuslineCommand = &cli.Command{
19+
Name: "statusline",
20+
Usage: "Output statusline for Claude Code (reads JSON from stdin)",
21+
Action: commandCCStatusline,
22+
}
23+
24+
func commandCCStatusline(c *cli.Context) error {
25+
// Hard timeout for entire operation - statusline must be fast
26+
ctx, cancel := context.WithTimeout(c.Context, 100*time.Millisecond)
27+
defer cancel()
28+
29+
// Read from stdin
30+
input, err := readStdinWithTimeout(ctx)
31+
if err != nil {
32+
outputFallback()
33+
return nil
34+
}
35+
36+
// Parse input
37+
var data model.CCStatuslineInput
38+
if err := json.Unmarshal(input, &data); err != nil {
39+
outputFallback()
40+
return nil
41+
}
42+
43+
// Calculate context percentage
44+
contextPercent := calculateContextPercent(data.ContextWindow)
45+
46+
// Get daily cost (cached) - need to read config first
47+
var dailyCost float64
48+
config, err := configService.ReadConfigFile(ctx)
49+
if err == nil {
50+
dailyCost = model.FetchDailyCostCached(ctx, config)
51+
}
52+
53+
// Format and output
54+
output := formatStatuslineOutput(data.Model.DisplayName, data.Cost.TotalCostUSD, dailyCost, contextPercent)
55+
fmt.Println(output)
56+
57+
return nil
58+
}
59+
60+
func readStdinWithTimeout(ctx context.Context) ([]byte, error) {
61+
resultCh := make(chan []byte, 1)
62+
errCh := make(chan error, 1)
63+
64+
go func() {
65+
reader := bufio.NewReader(os.Stdin)
66+
var data []byte
67+
for {
68+
line, err := reader.ReadBytes('\n')
69+
data = append(data, line...)
70+
if err != nil {
71+
if err == io.EOF {
72+
break
73+
}
74+
errCh <- err
75+
return
76+
}
77+
}
78+
resultCh <- data
79+
}()
80+
81+
select {
82+
case <-ctx.Done():
83+
return nil, ctx.Err()
84+
case err := <-errCh:
85+
return nil, err
86+
case data := <-resultCh:
87+
return data, nil
88+
}
89+
}
90+
91+
func calculateContextPercent(cw model.CCStatuslineContextWindow) float64 {
92+
if cw.ContextWindowSize == 0 {
93+
return 0
94+
}
95+
96+
// Use current_usage if available for accurate context window state
97+
if cw.CurrentUsage != nil {
98+
currentTokens := cw.CurrentUsage.InputTokens +
99+
cw.CurrentUsage.CacheCreationInputTokens +
100+
cw.CurrentUsage.CacheReadInputTokens
101+
return float64(currentTokens) / float64(cw.ContextWindowSize) * 100
102+
}
103+
104+
// Fallback to total tokens
105+
currentTokens := cw.TotalInputTokens + cw.TotalOutputTokens
106+
return float64(currentTokens) / float64(cw.ContextWindowSize) * 100
107+
}
108+
109+
func formatStatuslineOutput(modelName string, sessionCost, dailyCost, contextPercent float64) string {
110+
var parts []string
111+
112+
// Model name
113+
modelStr := fmt.Sprintf("🤖 %s", modelName)
114+
parts = append(parts, modelStr)
115+
116+
// Session cost (cyan)
117+
sessionStr := color.Cyan.Sprintf("💰 $%.2f", sessionCost)
118+
parts = append(parts, sessionStr)
119+
120+
// Daily cost (yellow)
121+
if dailyCost > 0 {
122+
dailyStr := color.Yellow.Sprintf("📊 $%.2f", dailyCost)
123+
parts = append(parts, dailyStr)
124+
} else {
125+
parts = append(parts, color.Gray.Sprint("📊 -"))
126+
}
127+
128+
// Context percentage with color coding
129+
var contextStr string
130+
switch {
131+
case contextPercent >= 80:
132+
contextStr = color.Red.Sprintf("📈 %.0f%%", contextPercent)
133+
case contextPercent >= 50:
134+
contextStr = color.Yellow.Sprintf("📈 %.0f%%", contextPercent)
135+
default:
136+
contextStr = color.Green.Sprintf("📈 %.0f%%", contextPercent)
137+
}
138+
parts = append(parts, contextStr)
139+
140+
return strings.Join(parts, " | ")
141+
}
142+
143+
func outputFallback() {
144+
fmt.Println(color.Gray.Sprint("🤖 - | 💰 - | 📊 - | 📈 -%"))
145+
}

docs/CC_STATUSLINE.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Claude Code Statusline Integration
2+
3+
Display real-time cost and context usage in Claude Code's status bar using ShellTime.
4+
5+
## Overview
6+
7+
The `shelltime cc statusline` command provides a custom status line for Claude Code that shows:
8+
9+
- Current model name
10+
- Session cost (current conversation)
11+
- Today's total cost (from ShellTime API)
12+
- Context window usage percentage
13+
14+
## Quick Start
15+
16+
### 1. Configure Claude Code
17+
18+
Add to your Claude Code settings (`~/.claude/settings.json`):
19+
20+
```json
21+
{
22+
"statusLine": {
23+
"type": "command",
24+
"command": "shelltime cc statusline"
25+
}
26+
}
27+
```
28+
29+
### 2. That's It!
30+
31+
The status line will appear at the bottom of Claude Code:
32+
33+
```
34+
🤖 Opus | 💰 $0.12 | 📊 $3.45 | 📈 45%
35+
```
36+
37+
---
38+
39+
## Output Format
40+
41+
| Section | Emoji | Description | Color |
42+
|---------|-------|-------------|-------|
43+
| Model | 🤖 | Current model display name | Default |
44+
| Session | 💰 | Current session cost in USD | Cyan |
45+
| Today | 📊 | Today's total cost from API | Yellow |
46+
| Context | 📈 | Context window usage % | Green/Yellow/Red |
47+
48+
### Context Color Coding
49+
50+
| Usage | Color | Meaning |
51+
|-------|-------|---------|
52+
| < 50% | Green | Plenty of context remaining |
53+
| 50-80% | Yellow | Context getting full |
54+
| > 80% | Red | Context nearly exhausted |
55+
56+
---
57+
58+
## How It Works
59+
60+
1. **Claude Code** passes session data as JSON via stdin
61+
2. **shelltime cc statusline** parses the JSON and extracts:
62+
- Model name from `model.display_name`
63+
- Session cost from `cost.total_cost_usd`
64+
- Context usage from `context_window`
65+
3. **Daily cost** is fetched from ShellTime GraphQL API (cached for 5 minutes)
66+
4. **Output** is a single formatted line with ANSI colors
67+
68+
### JSON Input (from Claude Code)
69+
70+
```json
71+
{
72+
"model": {
73+
"id": "claude-opus-4-1",
74+
"display_name": "Opus"
75+
},
76+
"cost": {
77+
"total_cost_usd": 0.12,
78+
"total_duration_ms": 45000
79+
},
80+
"context_window": {
81+
"total_input_tokens": 15234,
82+
"total_output_tokens": 4521,
83+
"context_window_size": 200000,
84+
"current_usage": {
85+
"input_tokens": 8500,
86+
"output_tokens": 1200,
87+
"cache_creation_input_tokens": 5000,
88+
"cache_read_input_tokens": 2000
89+
}
90+
}
91+
}
92+
```
93+
94+
---
95+
96+
## Requirements
97+
98+
### For Session Cost & Context
99+
100+
No additional setup required - data comes directly from Claude Code.
101+
102+
### For Today's Cost
103+
104+
Requires ShellTime configuration:
105+
106+
```yaml
107+
# ~/.shelltime/config.yaml
108+
token: "your-api-token"
109+
apiEndpoint: "https://api.shelltime.xyz"
110+
111+
# Enable OTEL collection to track costs
112+
aiCodeOtel:
113+
enabled: true
114+
```
115+
116+
If no token is configured, the daily cost will show as `-`.
117+
118+
---
119+
120+
## Performance
121+
122+
- **Hard timeout:** 100ms for entire operation
123+
- **API caching:** 5-minute TTL to minimize API calls
124+
- **Non-blocking:** Background API fetches don't delay output
125+
- **Graceful degradation:** Shows available data even if API fails
126+
127+
---
128+
129+
## Troubleshooting
130+
131+
### Status line not appearing
132+
133+
1. Check Claude Code settings path: `~/.claude/settings.json`
134+
2. Verify shelltime is in your PATH: `which shelltime`
135+
3. Test manually: `echo '{}' | shelltime cc statusline`
136+
137+
### Daily cost shows `-`
138+
139+
1. Verify your token is configured: `shelltime doctor`
140+
2. Check AICodeOtel is enabled in your config
141+
3. Ensure the daemon is running: `shelltime daemon status`
142+
143+
### Colors not displaying
144+
145+
Your terminal may not support ANSI colors. Check terminal settings or try a different terminal emulator.
146+
147+
---
148+
149+
## Related
150+
151+
- [Configuration Guide](./CONFIG.md) - Full configuration reference
152+
- [Claude Code Integration](./CONFIG.md#claude-code-integration) - AICodeOtel setup

0 commit comments

Comments
 (0)