cc-costline 0.4.0 → 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.d.ts +2 -0
- package/dist/statusline.js +67 -32
- package/package.json +1 -1
package/dist/statusline.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { CacheData } from "./cache.js";
|
|
1
2
|
export declare function formatTokens(t: number): string;
|
|
2
3
|
export declare function formatCost(n: number): string;
|
|
3
4
|
export declare function ctxColor(pct: number): string;
|
|
4
5
|
export declare function formatCountdown(resetsAtMs: number): string;
|
|
6
|
+
export declare function shouldRefreshLocalCostCache(cache: CacheData | null, transcriptPath?: string, now?: number): boolean;
|
|
5
7
|
export declare function rankColor(rank: number): string;
|
|
6
8
|
export declare function render(input: string): string;
|
package/dist/statusline.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { readFileSync, existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
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";
|
|
@@ -49,23 +51,45 @@ export function formatCountdown(resetsAtMs) {
|
|
|
49
51
|
const minutes = totalMinutes % 60;
|
|
50
52
|
return `-${hours}:${String(minutes).padStart(2, "0")}`;
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
export function shouldRefreshLocalCostCache(cache, transcriptPath = "", now = Date.now()) {
|
|
55
|
+
if (!cache)
|
|
56
|
+
return true;
|
|
57
|
+
const cacheUpdatedAt = new Date(cache.updatedAt).getTime();
|
|
58
|
+
if (isNaN(cacheUpdatedAt))
|
|
59
|
+
return true;
|
|
60
|
+
if (transcriptPath) {
|
|
61
|
+
try {
|
|
62
|
+
const transcriptMtime = statSync(transcriptPath).mtimeMs;
|
|
63
|
+
if (transcriptMtime > cacheUpdatedAt)
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
}
|
|
68
|
+
return now - cacheUpdatedAt >= CACHE_TTL_MS;
|
|
69
|
+
}
|
|
70
|
+
// ccclub rank fetcher — split cache: data persists, retry throttled
|
|
53
71
|
function getCcclubRank() {
|
|
54
72
|
const configPath = join(homedir(), ".ccclub", "config.json");
|
|
55
73
|
if (!existsSync(configPath))
|
|
56
74
|
return null;
|
|
57
75
|
const cacheFile = "/tmp/sl-ccclub-rank";
|
|
76
|
+
const now = Date.now();
|
|
58
77
|
let staleData = null;
|
|
78
|
+
let lastAttempt = 0;
|
|
59
79
|
if (existsSync(cacheFile)) {
|
|
60
80
|
try {
|
|
61
81
|
const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
62
82
|
staleData = cached.data ?? null;
|
|
63
|
-
|
|
64
|
-
if (
|
|
83
|
+
lastAttempt = cached.lastAttempt || cached.timestamp || 0;
|
|
84
|
+
if (now - lastAttempt < API_RETRY_TTL_MS)
|
|
65
85
|
return staleData;
|
|
66
86
|
}
|
|
67
87
|
catch { }
|
|
68
88
|
}
|
|
89
|
+
try {
|
|
90
|
+
writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: staleData ? (lastAttempt || now) : 0, lastAttempt: now }), "utf-8");
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
69
93
|
try {
|
|
70
94
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
71
95
|
const code = config.groups?.[0];
|
|
@@ -83,14 +107,10 @@ function getCcclubRank() {
|
|
|
83
107
|
if (!me)
|
|
84
108
|
return staleData;
|
|
85
109
|
const result = { rank: me.rank, total: rankings.length, cost: me.costUSD };
|
|
86
|
-
writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp:
|
|
110
|
+
writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp: now, lastAttempt: now }), "utf-8");
|
|
87
111
|
return result;
|
|
88
112
|
}
|
|
89
113
|
catch {
|
|
90
|
-
try {
|
|
91
|
-
writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: Date.now() }), "utf-8");
|
|
92
|
-
}
|
|
93
|
-
catch { }
|
|
94
114
|
return staleData;
|
|
95
115
|
}
|
|
96
116
|
}
|
|
@@ -103,44 +123,65 @@ export function rankColor(rank) {
|
|
|
103
123
|
return FG_ORANGE;
|
|
104
124
|
return FG_CYAN;
|
|
105
125
|
}
|
|
106
|
-
// Claude usage fetcher —
|
|
126
|
+
// Claude usage fetcher — token-aware cache: detects token rotation to retry immediately
|
|
107
127
|
function getClaudeUsage() {
|
|
108
128
|
const cacheFile = "/tmp/sl-claude-usage";
|
|
109
129
|
const hitFile = "/tmp/sl-claude-usage-hit";
|
|
110
130
|
const now = Date.now();
|
|
131
|
+
// Cache format: { data, lastAttempt, tokenPrefix (first 20 chars of token) }
|
|
111
132
|
let staleData = null;
|
|
133
|
+
let lastAttempt = 0;
|
|
134
|
+
let cachedTokenPrefix = "";
|
|
112
135
|
if (existsSync(cacheFile)) {
|
|
113
136
|
try {
|
|
114
137
|
const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
115
138
|
staleData = cached.data ?? null;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return staleData;
|
|
139
|
+
lastAttempt = cached.lastAttempt || 0;
|
|
140
|
+
cachedTokenPrefix = cached.tokenPrefix || "";
|
|
119
141
|
}
|
|
120
142
|
catch { }
|
|
121
143
|
}
|
|
144
|
+
// Get current token
|
|
145
|
+
let accessToken = "";
|
|
122
146
|
try {
|
|
123
147
|
const username = process.env.USER || process.env.USERNAME;
|
|
124
148
|
const keychainCmd = `security find-generic-password -s "Claude Code-credentials" -a "${username}" -w 2>/dev/null`;
|
|
125
149
|
const credentialsJSON = execSync(keychainCmd, { encoding: "utf-8", timeout: 2000 }).trim();
|
|
126
150
|
if (!credentialsJSON)
|
|
127
|
-
return
|
|
151
|
+
return staleData;
|
|
128
152
|
const credentials = JSON.parse(credentialsJSON);
|
|
129
|
-
|
|
153
|
+
accessToken = credentials.claudeAiOauth?.accessToken || "";
|
|
130
154
|
if (!accessToken)
|
|
131
|
-
return
|
|
155
|
+
return staleData;
|
|
132
156
|
const expiresAt = credentials.claudeAiOauth?.expiresAt;
|
|
133
|
-
if (expiresAt &&
|
|
134
|
-
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 {
|
|
135
174
|
const apiUrl = "https://api.anthropic.com/api/oauth/usage";
|
|
136
175
|
const curlCmd = `curl -sf "${apiUrl}" -H "Authorization: Bearer ${accessToken}" -H "anthropic-beta: oauth-2025-04-20"`;
|
|
137
176
|
const response = execSync(curlCmd, { encoding: "utf-8", timeout: 5000 });
|
|
138
|
-
if (!response)
|
|
139
|
-
// API failed — write cache with null data to prevent retry flood
|
|
140
|
-
writeFileSync(cacheFile, JSON.stringify({ data: null, timestamp: now }), "utf-8");
|
|
177
|
+
if (!response)
|
|
141
178
|
return staleData;
|
|
142
|
-
}
|
|
143
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 { }
|
|
144
185
|
const parseUtil = (val) => {
|
|
145
186
|
if (typeof val === "number")
|
|
146
187
|
return Math.round(val);
|
|
@@ -184,15 +225,10 @@ function getClaudeUsage() {
|
|
|
184
225
|
const result = { fiveHour, sevenDay };
|
|
185
226
|
if (fiveHourResetsAt)
|
|
186
227
|
result.fiveHourResetsAt = fiveHourResetsAt;
|
|
187
|
-
writeFileSync(cacheFile, JSON.stringify({ data: result,
|
|
228
|
+
writeFileSync(cacheFile, JSON.stringify({ data: result, lastAttempt: now, tokenPrefix: currentTokenPrefix }), "utf-8");
|
|
188
229
|
return result;
|
|
189
230
|
}
|
|
190
231
|
catch {
|
|
191
|
-
// Write cache with stale/null data to prevent retry flood on persistent failures
|
|
192
|
-
try {
|
|
193
|
-
writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: now }), "utf-8");
|
|
194
|
-
}
|
|
195
|
-
catch { }
|
|
196
232
|
return staleData;
|
|
197
233
|
}
|
|
198
234
|
}
|
|
@@ -206,7 +242,7 @@ export function render(input) {
|
|
|
206
242
|
}
|
|
207
243
|
// Session data from Claude Code stdin
|
|
208
244
|
const cost = data.cost?.total_cost_usd ?? 0;
|
|
209
|
-
const model = data.model?.display_name ?? "—";
|
|
245
|
+
const model = (data.model?.display_name ?? "—").replace(/\s*\((\d+[KMB])\s+context\)/i, " ($1)");
|
|
210
246
|
const contextPct = Math.floor(data.context_window?.used_percentage ?? 0);
|
|
211
247
|
const transcriptPath = data.transcript_path ?? "";
|
|
212
248
|
// Token stats from transcript
|
|
@@ -231,8 +267,7 @@ export function render(input) {
|
|
|
231
267
|
}
|
|
232
268
|
// Refresh local cost cache if stale
|
|
233
269
|
let cache = readCache();
|
|
234
|
-
|
|
235
|
-
if (cacheAge >= CACHE_TTL_MS) {
|
|
270
|
+
if (shouldRefreshLocalCostCache(cache, transcriptPath)) {
|
|
236
271
|
try {
|
|
237
272
|
const result = collectCosts();
|
|
238
273
|
// Don't overwrite valid cache with zeros (directory read failure)
|