cc-costline 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -52
- package/dist/cli.js +0 -0
- package/dist/statusline.js +123 -33
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,71 +1,63 @@
|
|
|
1
1
|
# cc-costline
|
|
2
2
|
|
|
3
|
-
Enhanced statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — adds
|
|
3
|
+
Enhanced statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — adds cost tracking, usage limits, and leaderboard rank to your terminal.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
|
|
6
|
+
14.6k ~ $2.42 / 40% by Opus 4.6 | 5h: 45% / 7d: 8% | 30d: $866 | #2/22 $67.0
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
## What it does
|
|
10
|
-
|
|
11
|
-
Claude Code's built-in statusline shows the current session cost. cc-costline extends it with:
|
|
12
|
-
|
|
13
|
-
- **Rolling cost totals** — see how much you've spent in the last 7 or 30 days
|
|
14
|
-
- **Token counts** — input/output token usage for the current session
|
|
15
|
-
- **Context window** — color-coded usage percentage (green → orange → red)
|
|
16
|
-
- **Code changes** — lines added/removed in the session
|
|
17
|
-
|
|
18
|
-
Cost calculation is self-contained — it reads your local transcript files and applies Anthropic's pricing table directly. No external API calls, no dependencies.
|
|
19
|
-
|
|
20
9
|
## Install
|
|
21
10
|
|
|
22
11
|
```bash
|
|
23
|
-
npm i -g cc-costline
|
|
24
|
-
cc-costline install
|
|
12
|
+
npm i -g cc-costline && cc-costline install
|
|
25
13
|
```
|
|
26
14
|
|
|
27
|
-
|
|
15
|
+
Open a new Claude Code session and you'll see the enhanced statusline. Requires Node.js >= 22.
|
|
16
|
+
|
|
17
|
+
## What you get
|
|
18
|
+
|
|
19
|
+
| Segment | Example | Description |
|
|
20
|
+
|---------|---------|-------------|
|
|
21
|
+
| Tokens ~ Cost / Context | `14.6k ~ $2.42 / 40% by Opus 4.6` | Session token count, cost, context usage, and model |
|
|
22
|
+
| Usage limits | `5h: 45% / 7d: 8%` | Claude 5-hour and 7-day utilization (auto-colored like context) |
|
|
23
|
+
| Period cost | `30d: $866` | Rolling cost total (configurable: 7d or 30d) |
|
|
24
|
+
| Leaderboard | `#2/22 $67.0` | [ccclub](https://github.com/mfbx9da4/ccclub) rank (if installed) |
|
|
25
|
+
|
|
26
|
+
### Colors
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
- **Context & usage limits** — green (< 60%) → orange (60-79%) → red (≥ 80%)
|
|
29
|
+
- **Leaderboard rank** — #1 gold, #2 white, #3 orange, others blue
|
|
30
|
+
- **Period cost** — yellow
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
### Optional integrations
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
3. Creates `~/.cc-costline/` with default config
|
|
36
|
-
4. Runs initial cost calculation from your transcript history
|
|
34
|
+
- **Claude usage limits** — reads OAuth credentials from macOS Keychain automatically. Just `claude login` and it works.
|
|
35
|
+
- **ccclub leaderboard** — install [ccclub](https://github.com/mfbx9da4/ccclub) (`npm i -g ccclub && ccclub init`). Rank appears automatically.
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
Both are zero-config: if not available, the segment is silently omitted.
|
|
39
38
|
|
|
40
39
|
## Commands
|
|
41
40
|
|
|
42
41
|
```bash
|
|
43
42
|
cc-costline install # Set up Claude Code integration
|
|
44
|
-
cc-costline uninstall # Remove from
|
|
43
|
+
cc-costline uninstall # Remove from settings
|
|
45
44
|
cc-costline refresh # Manually recalculate cost cache
|
|
46
|
-
cc-costline config --period
|
|
47
|
-
cc-costline config --period
|
|
48
|
-
cc-costline config --period both # Show both
|
|
45
|
+
cc-costline config --period 30d # Show 30-day cost (default)
|
|
46
|
+
cc-costline config --period 7d # Show 7-day cost
|
|
49
47
|
```
|
|
50
48
|
|
|
51
|
-
##
|
|
49
|
+
## How it works
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
1. `install` configures `~/.claude/settings.json` — sets the statusline command and adds session-end hooks for auto-refresh. Your existing settings are preserved.
|
|
52
|
+
2. `render` reads Claude Code's stdin JSON and the cost cache, outputs the formatted statusline.
|
|
53
|
+
3. `refresh` scans `~/.claude/projects/**/*.jsonl`, extracts token usage, applies per-model pricing, and writes to `~/.cc-costline/cache.json`.
|
|
54
|
+
4. Claude usage is fetched from `api.anthropic.com/api/oauth/usage` with a 60s file cache at `/tmp/sl-claude-usage`.
|
|
55
|
+
5. ccclub rank is fetched from `ccclub.dev/api/rank` with a 120s file cache at `/tmp/sl-ccclub-rank`.
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
| Token counts | Current session transcript | Gray |
|
|
60
|
-
| Session cost | Claude Code stdin | Yellow |
|
|
61
|
-
| Period cost | Cached calculation | Cyan |
|
|
62
|
-
| Lines changed | Claude Code stdin | Green / Gray |
|
|
63
|
-
| Context % | Claude Code stdin | Green (<60%) / Orange (60-80%) / Red (80%+) |
|
|
64
|
-
| Model name | Claude Code stdin | Brown |
|
|
57
|
+
<details>
|
|
58
|
+
<summary>Pricing table</summary>
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
cc-costline scans all `.jsonl` files under `~/.claude/projects/`, extracts token usage from assistant messages, and applies per-model pricing:
|
|
60
|
+
Prices per million tokens (USD):
|
|
69
61
|
|
|
70
62
|
| Model | Input | Output | Cache Write | Cache Read |
|
|
71
63
|
|-------|------:|-------:|------------:|-----------:|
|
|
@@ -77,17 +69,9 @@ cc-costline scans all `.jsonl` files under `~/.claude/projects/`, extracts token
|
|
|
77
69
|
| Haiku 4.5 | $1 | $5 | $1.25 | $0.10 |
|
|
78
70
|
| Haiku 3.5 | $0.80 | $4 | $1.00 | $0.08 |
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Entries are deduplicated by `sessionId:requestId` to avoid double-counting.
|
|
72
|
+
Unknown models fall back by family name, defaulting to Sonnet pricing.
|
|
83
73
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
~/.cc-costline/
|
|
88
|
-
├── cache.json # { cost7d, cost30d, updatedAt }
|
|
89
|
-
└── config.json # { period: "7d" | "30d" | "both" }
|
|
90
|
-
```
|
|
74
|
+
</details>
|
|
91
75
|
|
|
92
76
|
## Uninstall
|
|
93
77
|
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/statusline.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, existsSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
2
5
|
import { readCache, readConfig } from "./cache.js";
|
|
3
6
|
// ANSI colors (matching original statusline.sh)
|
|
4
7
|
const FG_GRAY = "\x1b[38;5;245m";
|
|
@@ -9,6 +12,7 @@ const FG_ORANGE = "\x1b[38;5;208m";
|
|
|
9
12
|
const FG_RED = "\x1b[38;5;167m";
|
|
10
13
|
const FG_MODEL = "\x1b[38;2;202;124;94m";
|
|
11
14
|
const FG_CYAN = "\x1b[38;5;109m";
|
|
15
|
+
const FG_WHITE = "\x1b[38;5;255m";
|
|
12
16
|
const RESET = "\x1b[0m";
|
|
13
17
|
function formatTokens(t) {
|
|
14
18
|
if (t >= 1_000_000)
|
|
@@ -33,6 +37,102 @@ function ctxColor(pct) {
|
|
|
33
37
|
return FG_ORANGE;
|
|
34
38
|
return FG_GREEN;
|
|
35
39
|
}
|
|
40
|
+
// ccclub rank fetcher with 120s file cache
|
|
41
|
+
function getCcclubRank() {
|
|
42
|
+
const configPath = join(homedir(), ".ccclub", "config.json");
|
|
43
|
+
if (!existsSync(configPath))
|
|
44
|
+
return null;
|
|
45
|
+
const cacheFile = "/tmp/sl-ccclub-rank";
|
|
46
|
+
const now = Date.now() / 1000;
|
|
47
|
+
if (existsSync(cacheFile)) {
|
|
48
|
+
const mtime = statSync(cacheFile).mtimeMs / 1000;
|
|
49
|
+
if (now - mtime <= 120) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
58
|
+
const code = config.groups?.[0];
|
|
59
|
+
const userId = config.userId;
|
|
60
|
+
if (!code || !userId)
|
|
61
|
+
return null;
|
|
62
|
+
const tz = -(new Date()).getTimezoneOffset();
|
|
63
|
+
const url = `${config.apiUrl}/api/rank/${code}?period=today&tz=${tz}`;
|
|
64
|
+
const response = execSync(`curl -sf "${url}"`, { encoding: "utf-8", timeout: 5000 });
|
|
65
|
+
if (!response)
|
|
66
|
+
return null;
|
|
67
|
+
const data = JSON.parse(response);
|
|
68
|
+
const rankings = data.rankings || [];
|
|
69
|
+
const me = rankings.find((r) => r.userId === userId);
|
|
70
|
+
if (!me)
|
|
71
|
+
return null;
|
|
72
|
+
const result = { rank: me.rank, total: rankings.length, cost: me.costUSD };
|
|
73
|
+
writeFileSync(cacheFile, JSON.stringify(result), "utf-8");
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function rankColor(rank) {
|
|
81
|
+
if (rank === 1)
|
|
82
|
+
return FG_YELLOW;
|
|
83
|
+
if (rank === 2)
|
|
84
|
+
return FG_WHITE;
|
|
85
|
+
if (rank === 3)
|
|
86
|
+
return FG_ORANGE;
|
|
87
|
+
return FG_CYAN;
|
|
88
|
+
}
|
|
89
|
+
// Claude usage fetcher with 60s file cache
|
|
90
|
+
function getClaudeUsage() {
|
|
91
|
+
const cacheFile = "/tmp/sl-claude-usage";
|
|
92
|
+
const now = Date.now() / 1000;
|
|
93
|
+
if (existsSync(cacheFile)) {
|
|
94
|
+
const mtime = statSync(cacheFile).mtimeMs / 1000;
|
|
95
|
+
if (now - mtime <= 60) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const username = process.env.USER || process.env.USERNAME;
|
|
104
|
+
const keychainCmd = `security find-generic-password -s "Claude Code-credentials" -a "${username}" -w 2>/dev/null`;
|
|
105
|
+
const credentialsJSON = execSync(keychainCmd, { encoding: "utf-8", timeout: 2000 }).trim();
|
|
106
|
+
if (!credentialsJSON)
|
|
107
|
+
return null;
|
|
108
|
+
const credentials = JSON.parse(credentialsJSON);
|
|
109
|
+
const accessToken = credentials.claudeAiOauth?.accessToken;
|
|
110
|
+
if (!accessToken)
|
|
111
|
+
return null;
|
|
112
|
+
const expiresAt = credentials.claudeAiOauth?.expiresAt;
|
|
113
|
+
if (expiresAt && Date.now() / 1000 > expiresAt)
|
|
114
|
+
return null;
|
|
115
|
+
const apiUrl = "https://api.anthropic.com/api/oauth/usage";
|
|
116
|
+
const curlCmd = `curl -sf "${apiUrl}" -H "Authorization: Bearer ${accessToken}" -H "anthropic-beta: oauth-2025-04-20" -H "User-Agent: claude-code/2.1.5"`;
|
|
117
|
+
const response = execSync(curlCmd, { encoding: "utf-8", timeout: 5000 });
|
|
118
|
+
if (!response)
|
|
119
|
+
return null;
|
|
120
|
+
const data = JSON.parse(response);
|
|
121
|
+
const parseUtil = (val) => {
|
|
122
|
+
if (typeof val === "number")
|
|
123
|
+
return Math.round(val);
|
|
124
|
+
if (typeof val === "string")
|
|
125
|
+
return Math.round(parseFloat(val.replace("%", "")));
|
|
126
|
+
return 0;
|
|
127
|
+
};
|
|
128
|
+
const result = { fiveHour: parseUtil(data.five_hour?.utilization), sevenDay: parseUtil(data.seven_day?.utilization) };
|
|
129
|
+
writeFileSync(cacheFile, JSON.stringify(result), "utf-8");
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
36
136
|
export function render(input) {
|
|
37
137
|
let data;
|
|
38
138
|
try {
|
|
@@ -43,13 +143,10 @@ export function render(input) {
|
|
|
43
143
|
}
|
|
44
144
|
// Session data from Claude Code stdin
|
|
45
145
|
const cost = data.cost?.total_cost_usd ?? 0;
|
|
46
|
-
const linesAdd = data.cost?.total_lines_added ?? 0;
|
|
47
|
-
const linesDel = data.cost?.total_lines_removed ?? 0;
|
|
48
146
|
const model = data.model?.display_name ?? "—";
|
|
49
147
|
const contextPct = Math.floor(data.context_window?.used_percentage ?? 0);
|
|
50
148
|
// Token stats from transcript
|
|
51
|
-
let
|
|
52
|
-
let outTokens = 0;
|
|
149
|
+
let totalTokens = 0;
|
|
53
150
|
const transcriptPath = data.transcript_path ?? "";
|
|
54
151
|
if (transcriptPath) {
|
|
55
152
|
try {
|
|
@@ -61,42 +158,35 @@ export function render(input) {
|
|
|
61
158
|
try {
|
|
62
159
|
const entry = JSON.parse(line);
|
|
63
160
|
if (entry.type === "assistant" && entry.message?.usage) {
|
|
64
|
-
|
|
65
|
-
outTokens += entry.message.usage.output_tokens || 0;
|
|
161
|
+
totalTokens += (entry.message.usage.input_tokens || 0) + (entry.message.usage.output_tokens || 0);
|
|
66
162
|
}
|
|
67
163
|
}
|
|
68
|
-
catch {
|
|
69
|
-
// skip malformed lines
|
|
70
|
-
}
|
|
164
|
+
catch { }
|
|
71
165
|
}
|
|
72
166
|
}
|
|
73
|
-
catch {
|
|
74
|
-
// transcript not readable
|
|
75
|
-
}
|
|
167
|
+
catch { }
|
|
76
168
|
}
|
|
77
|
-
const inFmt = formatTokens(inTokens);
|
|
78
|
-
const outFmt = formatTokens(outTokens);
|
|
79
|
-
// Cached cost data
|
|
80
169
|
const cache = readCache();
|
|
81
170
|
const config = readConfig();
|
|
82
|
-
|
|
171
|
+
const claudeUsage = getClaudeUsage();
|
|
172
|
+
const parts = [];
|
|
173
|
+
// 1. Tokens ~ cost / context % + model
|
|
174
|
+
parts.push(`${FG_GRAY_DIM}${formatTokens(totalTokens)}${RESET} ${FG_GRAY_DIM}~${RESET} ${FG_YELLOW}${formatCost(cost)}${RESET} ${FG_GRAY_DIM}/${RESET} ${ctxColor(contextPct)}${contextPct}%${RESET} ${FG_GRAY_DIM}by${RESET} ${FG_MODEL}${model}${RESET}`);
|
|
175
|
+
// 2. Claude usage limits (colored by utilization)
|
|
176
|
+
if (claudeUsage) {
|
|
177
|
+
parts.push(`${ctxColor(claudeUsage.fiveHour)}5h: ${claudeUsage.fiveHour}%${RESET} ${FG_GRAY_DIM}/${RESET} ${ctxColor(claudeUsage.sevenDay)}7d: ${claudeUsage.sevenDay}%${RESET}`);
|
|
178
|
+
}
|
|
179
|
+
// 3. Period cost (default 30d, configurable)
|
|
83
180
|
if (cache) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
181
|
+
const period = config.period || "30d";
|
|
182
|
+
const periodCost = period === "7d" ? cache.cost7d : cache.cost30d;
|
|
183
|
+
parts.push(`${FG_YELLOW}${period}: ${formatCost(periodCost)}${RESET}`);
|
|
184
|
+
}
|
|
185
|
+
// 4. ccclub rank (colored by position)
|
|
186
|
+
const ccclubRank = getCcclubRank();
|
|
187
|
+
if (ccclubRank) {
|
|
188
|
+
const rc = rankColor(ccclubRank.rank);
|
|
189
|
+
parts.push(`${rc}#${ccclubRank.rank}/${ccclubRank.total} ${formatCost(ccclubRank.cost)}${RESET}`);
|
|
94
190
|
}
|
|
95
|
-
const parts = [
|
|
96
|
-
`${FG_GRAY_DIM}Token: ↑${inFmt} ↓${outFmt}${RESET}`,
|
|
97
|
-
`${FG_YELLOW}${formatCost(cost)}${costSuffix}${RESET}`,
|
|
98
|
-
`${FG_GRAY_DIM}Code: ${FG_GREEN}+${linesAdd}${RESET} ${FG_GRAY_DIM}-${linesDel}${RESET}`,
|
|
99
|
-
`${ctxColor(contextPct)}${contextPct}%${RESET} ${FG_GRAY_DIM}by${RESET} ${FG_MODEL}${model}${RESET}`,
|
|
100
|
-
];
|
|
101
191
|
return "\n " + parts.join(` ${FG_GRAY}|${RESET} `) + "\n";
|
|
102
192
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-costline",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Enhanced statusline for Claude Code with
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Enhanced statusline for Claude Code with cost tracking, usage limits, and leaderboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-costline": "dist/cli.js"
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
"author": "ventuss",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"typescript": "^5.
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
28
|
}
|
|
29
29
|
}
|