cc-costline 0.4.1 → 0.4.2
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/dist/statusline.js +49 -29
- package/package.json +1 -1
package/dist/statusline.js
CHANGED
|
@@ -4,8 +4,10 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { readCache, writeCache, readConfig } from "./cache.js";
|
|
6
6
|
import { collectCosts } from "./collector.js";
|
|
7
|
-
//
|
|
7
|
+
// TTL for local cost cache (2 minutes)
|
|
8
8
|
const CACHE_TTL_MS = 120_000;
|
|
9
|
+
// TTL for external API retry throttle (5 minutes) — usage API has strict per-token rate limits (~5 req/token)
|
|
10
|
+
const API_RETRY_TTL_MS = 300_000;
|
|
9
11
|
// ANSI colors (matching original statusline.sh)
|
|
10
12
|
const FG_GRAY = "\x1b[38;5;245m";
|
|
11
13
|
const FG_GRAY_DIM = "\x1b[38;5;102m";
|
|
@@ -65,23 +67,29 @@ export function shouldRefreshLocalCostCache(cache, transcriptPath = "", now = Da
|
|
|
65
67
|
}
|
|
66
68
|
return now - cacheUpdatedAt >= CACHE_TTL_MS;
|
|
67
69
|
}
|
|
68
|
-
// ccclub rank fetcher —
|
|
70
|
+
// ccclub rank fetcher — split cache: data persists, retry throttled
|
|
69
71
|
function getCcclubRank() {
|
|
70
72
|
const configPath = join(homedir(), ".ccclub", "config.json");
|
|
71
73
|
if (!existsSync(configPath))
|
|
72
74
|
return null;
|
|
73
75
|
const cacheFile = "/tmp/sl-ccclub-rank";
|
|
76
|
+
const now = Date.now();
|
|
74
77
|
let staleData = null;
|
|
78
|
+
let lastAttempt = 0;
|
|
75
79
|
if (existsSync(cacheFile)) {
|
|
76
80
|
try {
|
|
77
81
|
const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
78
82
|
staleData = cached.data ?? null;
|
|
79
|
-
|
|
80
|
-
if (
|
|
83
|
+
lastAttempt = cached.lastAttempt || cached.timestamp || 0;
|
|
84
|
+
if (now - lastAttempt < API_RETRY_TTL_MS)
|
|
81
85
|
return staleData;
|
|
82
86
|
}
|
|
83
87
|
catch { }
|
|
84
88
|
}
|
|
89
|
+
try {
|
|
90
|
+
writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: staleData ? (lastAttempt || now) : 0, lastAttempt: now }), "utf-8");
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
85
93
|
try {
|
|
86
94
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
87
95
|
const code = config.groups?.[0];
|
|
@@ -99,14 +107,10 @@ function getCcclubRank() {
|
|
|
99
107
|
if (!me)
|
|
100
108
|
return staleData;
|
|
101
109
|
const result = { rank: me.rank, total: rankings.length, cost: me.costUSD };
|
|
102
|
-
writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp:
|
|
110
|
+
writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp: now, lastAttempt: now }), "utf-8");
|
|
103
111
|
return result;
|
|
104
112
|
}
|
|
105
113
|
catch {
|
|
106
|
-
try {
|
|
107
|
-
writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: Date.now() }), "utf-8");
|
|
108
|
-
}
|
|
109
|
-
catch { }
|
|
110
114
|
return staleData;
|
|
111
115
|
}
|
|
112
116
|
}
|
|
@@ -119,44 +123,65 @@ export function rankColor(rank) {
|
|
|
119
123
|
return FG_ORANGE;
|
|
120
124
|
return FG_CYAN;
|
|
121
125
|
}
|
|
122
|
-
// Claude usage fetcher —
|
|
126
|
+
// Claude usage fetcher — token-aware cache: detects token rotation to retry immediately
|
|
123
127
|
function getClaudeUsage() {
|
|
124
128
|
const cacheFile = "/tmp/sl-claude-usage";
|
|
125
129
|
const hitFile = "/tmp/sl-claude-usage-hit";
|
|
126
130
|
const now = Date.now();
|
|
131
|
+
// Cache format: { data, lastAttempt, tokenPrefix (first 20 chars of token) }
|
|
127
132
|
let staleData = null;
|
|
133
|
+
let lastAttempt = 0;
|
|
134
|
+
let cachedTokenPrefix = "";
|
|
128
135
|
if (existsSync(cacheFile)) {
|
|
129
136
|
try {
|
|
130
137
|
const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
131
138
|
staleData = cached.data ?? null;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return staleData;
|
|
139
|
+
lastAttempt = cached.lastAttempt || 0;
|
|
140
|
+
cachedTokenPrefix = cached.tokenPrefix || "";
|
|
135
141
|
}
|
|
136
142
|
catch { }
|
|
137
143
|
}
|
|
144
|
+
// Get current token
|
|
145
|
+
let accessToken = "";
|
|
138
146
|
try {
|
|
139
147
|
const username = process.env.USER || process.env.USERNAME;
|
|
140
148
|
const keychainCmd = `security find-generic-password -s "Claude Code-credentials" -a "${username}" -w 2>/dev/null`;
|
|
141
149
|
const credentialsJSON = execSync(keychainCmd, { encoding: "utf-8", timeout: 2000 }).trim();
|
|
142
150
|
if (!credentialsJSON)
|
|
143
|
-
return
|
|
151
|
+
return staleData;
|
|
144
152
|
const credentials = JSON.parse(credentialsJSON);
|
|
145
|
-
|
|
153
|
+
accessToken = credentials.claudeAiOauth?.accessToken || "";
|
|
146
154
|
if (!accessToken)
|
|
147
|
-
return
|
|
155
|
+
return staleData;
|
|
148
156
|
const expiresAt = credentials.claudeAiOauth?.expiresAt;
|
|
149
|
-
if (expiresAt &&
|
|
150
|
-
return
|
|
157
|
+
if (expiresAt && now > expiresAt)
|
|
158
|
+
return staleData;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return staleData;
|
|
162
|
+
}
|
|
163
|
+
const currentTokenPrefix = accessToken.slice(-20);
|
|
164
|
+
const tokenChanged = cachedTokenPrefix && currentTokenPrefix !== cachedTokenPrefix;
|
|
165
|
+
// Throttle: skip API call unless token rotated or TTL expired
|
|
166
|
+
if (!tokenChanged && lastAttempt && now - lastAttempt < API_RETRY_TTL_MS)
|
|
167
|
+
return staleData;
|
|
168
|
+
// Mark attempt and store token prefix
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(cacheFile, JSON.stringify({ data: staleData, lastAttempt: now, tokenPrefix: currentTokenPrefix }), "utf-8");
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
try {
|
|
151
174
|
const apiUrl = "https://api.anthropic.com/api/oauth/usage";
|
|
152
175
|
const curlCmd = `curl -sf "${apiUrl}" -H "Authorization: Bearer ${accessToken}" -H "anthropic-beta: oauth-2025-04-20"`;
|
|
153
176
|
const response = execSync(curlCmd, { encoding: "utf-8", timeout: 5000 });
|
|
154
|
-
if (!response)
|
|
155
|
-
// API failed — write cache with null data to prevent retry flood
|
|
156
|
-
writeFileSync(cacheFile, JSON.stringify({ data: null, timestamp: now }), "utf-8");
|
|
177
|
+
if (!response)
|
|
157
178
|
return staleData;
|
|
158
|
-
}
|
|
159
179
|
const data = JSON.parse(response);
|
|
180
|
+
// Debug: save raw API response for diagnosis
|
|
181
|
+
try {
|
|
182
|
+
writeFileSync("/tmp/sl-claude-usage-raw", JSON.stringify(data, null, 2), "utf-8");
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
160
185
|
const parseUtil = (val) => {
|
|
161
186
|
if (typeof val === "number")
|
|
162
187
|
return Math.round(val);
|
|
@@ -200,15 +225,10 @@ function getClaudeUsage() {
|
|
|
200
225
|
const result = { fiveHour, sevenDay };
|
|
201
226
|
if (fiveHourResetsAt)
|
|
202
227
|
result.fiveHourResetsAt = fiveHourResetsAt;
|
|
203
|
-
writeFileSync(cacheFile, JSON.stringify({ data: result,
|
|
228
|
+
writeFileSync(cacheFile, JSON.stringify({ data: result, lastAttempt: now, tokenPrefix: currentTokenPrefix }), "utf-8");
|
|
204
229
|
return result;
|
|
205
230
|
}
|
|
206
231
|
catch {
|
|
207
|
-
// Write cache with stale/null data to prevent retry flood on persistent failures
|
|
208
|
-
try {
|
|
209
|
-
writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: now }), "utf-8");
|
|
210
|
-
}
|
|
211
|
-
catch { }
|
|
212
232
|
return staleData;
|
|
213
233
|
}
|
|
214
234
|
}
|
|
@@ -222,7 +242,7 @@ export function render(input) {
|
|
|
222
242
|
}
|
|
223
243
|
// Session data from Claude Code stdin
|
|
224
244
|
const cost = data.cost?.total_cost_usd ?? 0;
|
|
225
|
-
const model = data.model?.display_name ?? "—";
|
|
245
|
+
const model = (data.model?.display_name ?? "—").replace(/\s*\((\d+[KMB])\s+context\)/i, " ($1)");
|
|
226
246
|
const contextPct = Math.floor(data.context_window?.used_percentage ?? 0);
|
|
227
247
|
const transcriptPath = data.transcript_path ?? "";
|
|
228
248
|
// Token stats from transcript
|