cc-costline 0.3.0 → 0.3.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 +54 -32
- package/package.json +1 -1
package/dist/statusline.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, existsSync,
|
|
1
|
+
import { readFileSync, existsSync, 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";
|
|
@@ -46,44 +46,52 @@ export function formatCountdown(resetsAtMs) {
|
|
|
46
46
|
const minutes = totalMinutes % 60;
|
|
47
47
|
return `-${hours}:${String(minutes).padStart(2, "0")}`;
|
|
48
48
|
}
|
|
49
|
-
// ccclub rank fetcher
|
|
50
|
-
function getCcclubRank() {
|
|
49
|
+
// ccclub rank fetcher — cached per session (stale fallback on failure)
|
|
50
|
+
function getCcclubRank(sessionId) {
|
|
51
51
|
const configPath = join(homedir(), ".ccclub", "config.json");
|
|
52
52
|
if (!existsSync(configPath))
|
|
53
53
|
return null;
|
|
54
54
|
const cacheFile = "/tmp/sl-ccclub-rank";
|
|
55
|
-
|
|
55
|
+
let staleData = null;
|
|
56
56
|
if (existsSync(cacheFile)) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
try {
|
|
58
|
+
const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
59
|
+
staleData = cached.data ?? null;
|
|
60
|
+
if (cached.sessionId === sessionId)
|
|
61
|
+
return staleData;
|
|
62
|
+
// If cache is less than 2 minutes old, reuse it to avoid rate limits
|
|
63
|
+
const cacheAge = Date.now() - (cached.timestamp || 0);
|
|
64
|
+
if (cacheAge < 600_000)
|
|
65
|
+
return staleData;
|
|
63
66
|
}
|
|
67
|
+
catch { }
|
|
64
68
|
}
|
|
65
69
|
try {
|
|
66
70
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
67
71
|
const code = config.groups?.[0];
|
|
68
72
|
const userId = config.userId;
|
|
69
73
|
if (!code || !userId)
|
|
70
|
-
return
|
|
74
|
+
return staleData;
|
|
71
75
|
const tz = -(new Date()).getTimezoneOffset();
|
|
72
76
|
const url = `${config.apiUrl}/api/rank/${code}?period=today&tz=${tz}`;
|
|
73
77
|
const response = execSync(`curl -sf "${url}"`, { encoding: "utf-8", timeout: 5000 });
|
|
74
78
|
if (!response)
|
|
75
|
-
return
|
|
79
|
+
return staleData;
|
|
76
80
|
const data = JSON.parse(response);
|
|
77
81
|
const rankings = data.rankings || [];
|
|
78
82
|
const me = rankings.find((r) => r.userId === userId);
|
|
79
83
|
if (!me)
|
|
80
|
-
return
|
|
84
|
+
return staleData;
|
|
81
85
|
const result = { rank: me.rank, total: rankings.length, cost: me.costUSD };
|
|
82
|
-
writeFileSync(cacheFile, JSON.stringify(result), "utf-8");
|
|
86
|
+
writeFileSync(cacheFile, JSON.stringify({ sessionId, data: result, timestamp: Date.now() }), "utf-8");
|
|
83
87
|
return result;
|
|
84
88
|
}
|
|
85
89
|
catch {
|
|
86
|
-
|
|
90
|
+
try {
|
|
91
|
+
writeFileSync(cacheFile, JSON.stringify({ sessionId, data: staleData, timestamp: Date.now() }), "utf-8");
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
return staleData;
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
export function rankColor(rank) {
|
|
@@ -95,20 +103,24 @@ export function rankColor(rank) {
|
|
|
95
103
|
return FG_ORANGE;
|
|
96
104
|
return FG_CYAN;
|
|
97
105
|
}
|
|
98
|
-
// Claude usage fetcher
|
|
99
|
-
function getClaudeUsage() {
|
|
106
|
+
// Claude usage fetcher — cached per session (stale fallback on failure)
|
|
107
|
+
function getClaudeUsage(sessionId) {
|
|
100
108
|
const cacheFile = "/tmp/sl-claude-usage";
|
|
101
109
|
const hitFile = "/tmp/sl-claude-usage-hit";
|
|
102
110
|
const now = Date.now();
|
|
103
|
-
|
|
111
|
+
let staleData = null;
|
|
104
112
|
if (existsSync(cacheFile)) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
try {
|
|
114
|
+
const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
115
|
+
staleData = cached.data ?? null;
|
|
116
|
+
if (cached.sessionId === sessionId)
|
|
117
|
+
return staleData;
|
|
118
|
+
// If cache is less than 2 minutes old, reuse it to avoid rate limits
|
|
119
|
+
const cacheAge = now - (cached.timestamp || 0);
|
|
120
|
+
if (cacheAge < 600_000)
|
|
121
|
+
return staleData;
|
|
111
122
|
}
|
|
123
|
+
catch { }
|
|
112
124
|
}
|
|
113
125
|
try {
|
|
114
126
|
const username = process.env.USER || process.env.USERNAME;
|
|
@@ -124,10 +136,13 @@ function getClaudeUsage() {
|
|
|
124
136
|
if (expiresAt && Date.now() / 1000 > expiresAt)
|
|
125
137
|
return null;
|
|
126
138
|
const apiUrl = "https://api.anthropic.com/api/oauth/usage";
|
|
127
|
-
const curlCmd = `curl -sf "${apiUrl}" -H "Authorization: Bearer ${accessToken}" -H "anthropic-beta: oauth-2025-04-20"
|
|
139
|
+
const curlCmd = `curl -sf "${apiUrl}" -H "Authorization: Bearer ${accessToken}" -H "anthropic-beta: oauth-2025-04-20"`;
|
|
128
140
|
const response = execSync(curlCmd, { encoding: "utf-8", timeout: 5000 });
|
|
129
|
-
if (!response)
|
|
130
|
-
|
|
141
|
+
if (!response) {
|
|
142
|
+
// API failed — write cache with null data to prevent retry flood
|
|
143
|
+
writeFileSync(cacheFile, JSON.stringify({ sessionId, data: null, timestamp: now }), "utf-8");
|
|
144
|
+
return staleData;
|
|
145
|
+
}
|
|
131
146
|
const data = JSON.parse(response);
|
|
132
147
|
const parseUtil = (val) => {
|
|
133
148
|
if (typeof val === "number")
|
|
@@ -172,11 +187,16 @@ function getClaudeUsage() {
|
|
|
172
187
|
const result = { fiveHour, sevenDay };
|
|
173
188
|
if (fiveHourResetsAt)
|
|
174
189
|
result.fiveHourResetsAt = fiveHourResetsAt;
|
|
175
|
-
writeFileSync(cacheFile, JSON.stringify(result), "utf-8");
|
|
190
|
+
writeFileSync(cacheFile, JSON.stringify({ sessionId, data: result, timestamp: now }), "utf-8");
|
|
176
191
|
return result;
|
|
177
192
|
}
|
|
178
193
|
catch {
|
|
179
|
-
|
|
194
|
+
// Write cache with stale/null data to prevent retry flood on persistent failures
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(cacheFile, JSON.stringify({ sessionId, data: staleData, timestamp: now }), "utf-8");
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
return staleData;
|
|
180
200
|
}
|
|
181
201
|
}
|
|
182
202
|
export function render(input) {
|
|
@@ -191,9 +211,11 @@ export function render(input) {
|
|
|
191
211
|
const cost = data.cost?.total_cost_usd ?? 0;
|
|
192
212
|
const model = data.model?.display_name ?? "—";
|
|
193
213
|
const contextPct = Math.floor(data.context_window?.used_percentage ?? 0);
|
|
214
|
+
// Session ID from transcript path (filename without extension)
|
|
215
|
+
const transcriptPath = data.transcript_path ?? "";
|
|
216
|
+
const sessionId = transcriptPath ? transcriptPath.replace(/^.*\//, "").replace(/\.jsonl$/, "") : "";
|
|
194
217
|
// Token stats from transcript
|
|
195
218
|
let totalTokens = 0;
|
|
196
|
-
const transcriptPath = data.transcript_path ?? "";
|
|
197
219
|
if (transcriptPath) {
|
|
198
220
|
try {
|
|
199
221
|
const content = readFileSync(transcriptPath, "utf-8");
|
|
@@ -214,7 +236,7 @@ export function render(input) {
|
|
|
214
236
|
}
|
|
215
237
|
const cache = readCache();
|
|
216
238
|
const config = readConfig();
|
|
217
|
-
const claudeUsage = getClaudeUsage();
|
|
239
|
+
const claudeUsage = getClaudeUsage(sessionId);
|
|
218
240
|
const g = FG_GRAY_DIM;
|
|
219
241
|
const y = FG_YELLOW;
|
|
220
242
|
const m = FG_MODEL;
|
|
@@ -247,7 +269,7 @@ export function render(input) {
|
|
|
247
269
|
segments.push(usageParts.join(` ${g}·${r} `));
|
|
248
270
|
}
|
|
249
271
|
// #2 $53.6
|
|
250
|
-
const ccclubRank = getCcclubRank();
|
|
272
|
+
const ccclubRank = getCcclubRank(sessionId);
|
|
251
273
|
if (ccclubRank) {
|
|
252
274
|
const rc = rankColor(ccclubRank.rank);
|
|
253
275
|
segments.push(`${rc}#${ccclubRank.rank} ${formatCost(ccclubRank.cost)}${r}`);
|