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 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 7-day and 30-day rolling cost tracking to your terminal.
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
- Token: ↑12.3k ↓45.6k | $2.34 (7d:$385) | Code: +12 -3 | 42% by Opus 4.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
- That's it. Open a new Claude Code session and you'll see the enhanced statusline.
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
- Requires Node.js >= 22.
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
- ## What `install` does
32
+ ### Optional integrations
32
33
 
33
- 1. Sets `statusLine.command` `cc-costline render` in `~/.claude/settings.json`
34
- 2. Adds `SessionEnd` and `Stop` hooks to auto-refresh the cost cache
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
- Your existing hooks and settings are preserved.
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 Claude Code settings
43
+ cc-costline uninstall # Remove from settings
45
44
  cc-costline refresh # Manually recalculate cost cache
46
- cc-costline config --period 7d # Show 7-day cost (default)
47
- cc-costline config --period 30d # Show 30-day cost
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
- ## Output format
49
+ ## How it works
52
50
 
53
- ```
54
- Token: ↑{in} ↓{out} | ${session} ({period}:${total}) | Code: +{add} -{del} | {ctx}% by {model}
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
- | Segment | Source | Color |
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
- ## How cost is calculated
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
- Prices are per million tokens in USD. Unknown models fall back by family name (opus/sonnet/haiku), defaulting to Sonnet pricing.
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
- ## Files
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
@@ -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 inTokens = 0;
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
- inTokens += entry.message.usage.input_tokens || 0;
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
- let costSuffix = "";
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 { period } = config;
85
- if (period === "7d") {
86
- costSuffix = ` ${FG_CYAN}(7d:${formatCost(cache.cost7d)})${RESET}`;
87
- }
88
- else if (period === "30d") {
89
- costSuffix = ` ${FG_CYAN}(30d:${formatCost(cache.cost30d)})${RESET}`;
90
- }
91
- else {
92
- costSuffix = ` ${FG_CYAN}(7d:${formatCost(cache.cost7d)} 30d:${formatCost(cache.cost30d)})${RESET}`;
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.1.1",
4
- "description": "Enhanced statusline for Claude Code with 7d/30d cost tracking",
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.7.0"
27
+ "typescript": "^5.9.3"
28
28
  }
29
29
  }