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.
Files changed (2) hide show
  1. package/dist/statusline.js +54 -32
  2. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync } from "node:fs";
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 with 120s file cache
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
- const now = Date.now() / 1000;
55
+ let staleData = null;
56
56
  if (existsSync(cacheFile)) {
57
- const mtime = statSync(cacheFile).mtimeMs / 1000;
58
- if (now - mtime <= 120) {
59
- try {
60
- return JSON.parse(readFileSync(cacheFile, "utf-8"));
61
- }
62
- catch { }
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 null;
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 null;
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 null;
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
- return null;
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 with 60s file cache
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
- const nowSec = now / 1000;
111
+ let staleData = null;
104
112
  if (existsSync(cacheFile)) {
105
- const mtime = statSync(cacheFile).mtimeMs / 1000;
106
- if (nowSec - mtime <= 60) {
107
- try {
108
- return JSON.parse(readFileSync(cacheFile, "utf-8"));
109
- }
110
- catch { }
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" -H "User-Agent: claude-code/2.1.5"`;
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
- return null;
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
- return null;
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-costline",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Enhanced statusline for Claude Code with cost tracking, usage limits, and leaderboard",
5
5
  "type": "module",
6
6
  "bin": {