cc-costline 0.2.3 → 0.3.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.es.md CHANGED
@@ -23,7 +23,7 @@ Abre una nueva sesión de Claude Code y verás la statusline mejorada. Requiere
23
23
  | Segmento | Ejemplo | Descripción |
24
24
  |----------|---------|-------------|
25
25
  | Tokens ~ Costo / Contexto | `14.6k ~ $2.42 / 40% by Opus 4.6` | Tokens de la sesión, costo, uso de contexto y modelo |
26
- | Límites de uso | `5h: 45% / 7d: 8%` | Utilización de Claude a 5 horas y 7 días (coloreado como el contexto) |
26
+ | Límites de uso | `5h: 45% / 7d: 8%` | Utilización de Claude a 5 horas y 7 días (coloreado como el contexto). Al 100%, muestra cuenta regresiva: `5h:-3:20` |
27
27
  | Costo del período | `30d: $866` | Costo acumulado (configurable: 7d o 30d) |
28
28
  | Ranking | `#2/22 $67.0` | Posición en [ccclub](https://github.com/mazzzystar/ccclub) (si está instalado) |
29
29
 
@@ -77,6 +77,12 @@ Los modelos desconocidos usan el precio de su familia, Sonnet por defecto.
77
77
 
78
78
  </details>
79
79
 
80
+ ## Desarrollo
81
+
82
+ ```bash
83
+ npm test # Build + ejecutar tests unitarios (node:test, sin dependencias)
84
+ ```
85
+
80
86
  ## Desinstalación
81
87
 
82
88
  ```bash
package/README.fr.md CHANGED
@@ -23,7 +23,7 @@ Ouvrez une nouvelle session Claude Code et la statusline enrichie apparaîtra. N
23
23
  | Segment | Exemple | Description |
24
24
  |---------|---------|-------------|
25
25
  | Tokens ~ Coût / Contexte | `14.6k ~ $2.42 / 40% by Opus 4.6` | Nombre de tokens, coût, utilisation du contexte et modèle |
26
- | Limites d'utilisation | `5h: 45% / 7d: 8%` | Utilisation Claude sur 5 heures et 7 jours (colorée comme le contexte) |
26
+ | Limites d'utilisation | `5h: 45% / 7d: 8%` | Utilisation Claude sur 5 heures et 7 jours (colorée comme le contexte). À 100 %, affiche un compte à rebours : `5h:-3:20` |
27
27
  | Coût périodique | `30d: $866` | Coût cumulé glissant (configurable : 7j ou 30j) |
28
28
  | Classement | `#2/22 $67.0` | Rang [ccclub](https://github.com/mazzzystar/ccclub) (si installé) |
29
29
 
@@ -77,6 +77,12 @@ Les modèles inconnus utilisent le prix de leur famille, Sonnet par défaut.
77
77
 
78
78
  </details>
79
79
 
80
+ ## Développement
81
+
82
+ ```bash
83
+ npm test # Build + exécuter les tests unitaires (node:test, zéro dépendance)
84
+ ```
85
+
80
86
  ## Désinstallation
81
87
 
82
88
  ```bash
package/README.ja.md CHANGED
@@ -23,7 +23,7 @@ npm i -g cc-costline && cc-costline install
23
23
  | セグメント | 例 | 説明 |
24
24
  |-----------|---|------|
25
25
  | トークン ~ コスト / コンテキスト | `14.6k ~ $2.42 / 40% by Opus 4.6` | セッションのトークン数、コスト、コンテキスト使用率、モデル |
26
- | 使用制限 | `5h: 45% / 7d: 8%` | Claude の 5 時間・7 日間の使用率(コンテキストと同じ色分け) |
26
+ | 使用制限 | `5h: 45% / 7d: 8%` | Claude の 5 時間・7 日間の使用率(コンテキストと同じ色分け)。100% 到達時はカウントダウン表示:`5h:-3:20` |
27
27
  | 期間コスト | `30d: $866` | ローリングコスト合計(7d または 30d で設定可能) |
28
28
  | リーダーボード | `#2/22 $67.0` | [ccclub](https://github.com/mazzzystar/ccclub) ランキング(インストール時) |
29
29
 
@@ -77,6 +77,12 @@ cc-costline config --period 7d # 7 日間のコストを表示
77
77
 
78
78
  </details>
79
79
 
80
+ ## 開発
81
+
82
+ ```bash
83
+ npm test # ビルド + ユニットテスト実行(node:test、依存関係なし)
84
+ ```
85
+
80
86
  ## アンインストール
81
87
 
82
88
  ```bash
package/README.md CHANGED
@@ -23,7 +23,7 @@ Open a new Claude Code session and you'll see the enhanced statusline. Requires
23
23
  | Segment | Example | Description |
24
24
  |---------|---------|-------------|
25
25
  | Tokens ~ Cost / Context | `14.6k ~ $2.42 / 40% by Opus 4.6` | Session token count, cost, context usage, and model |
26
- | Usage limits | `5h: 45% / 7d: 8%` | Claude 5-hour and 7-day utilization (auto-colored like context) |
26
+ | Usage limits | `5h: 45% / 7d: 8%` | Claude 5-hour and 7-day utilization (auto-colored like context). At 100%, shows countdown: `5h:-3:20` |
27
27
  | Period cost | `30d: $866` | Rolling cost total (configurable: 7d or 30d) |
28
28
  | Leaderboard | `#2/22 $67.0` | [ccclub](https://github.com/mazzzystar/ccclub) rank (if installed) |
29
29
 
@@ -77,6 +77,12 @@ Unknown models fall back by family name, defaulting to Sonnet pricing.
77
77
 
78
78
  </details>
79
79
 
80
+ ## Development
81
+
82
+ ```bash
83
+ npm test # Build + run unit tests (node:test, zero dependencies)
84
+ ```
85
+
80
86
  ## Uninstall
81
87
 
82
88
  ```bash
package/README.zh-CN.md CHANGED
@@ -23,7 +23,7 @@ npm i -g cc-costline && cc-costline install
23
23
  | 模块 | 示例 | 说明 |
24
24
  |------|------|------|
25
25
  | Token ~ 费用 / 上下文 | `14.6k ~ $2.42 / 40% by Opus 4.6` | 会话 token 数量、费用、上下文使用率和模型 |
26
- | 使用限额 | `5h: 45% / 7d: 8%` | Claude 5 小时和 7 天使用率(颜色同上下文) |
26
+ | 使用限额 | `5h: 45% / 7d: 8%` | Claude 5 小时和 7 天使用率(颜色同上下文)。达到 100% 时显示倒计时:`5h:-3:20` |
27
27
  | 周期费用 | `30d: $866` | 滚动费用合计(可配置:7d 或 30d) |
28
28
  | 排行榜 | `#2/22 $67.0` | [ccclub](https://github.com/mazzzystar/ccclub) 排名(需安装) |
29
29
 
@@ -77,6 +77,12 @@ cc-costline config --period 7d # 显示 7 天费用
77
77
 
78
78
  </details>
79
79
 
80
+ ## 开发
81
+
82
+ ```bash
83
+ npm test # 构建 + 运行单元测试(node:test,零依赖)
84
+ ```
85
+
80
86
  ## 卸载
81
87
 
82
88
  ```bash
@@ -1 +1,6 @@
1
+ export declare function formatTokens(t: number): string;
2
+ export declare function formatCost(n: number): string;
3
+ export declare function ctxColor(pct: number): string;
4
+ export declare function formatCountdown(resetsAtMs: number): string;
5
+ export declare function rankColor(rank: number): string;
1
6
  export declare function render(input: string): string;
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, statSync, writeFileSync } 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";
@@ -14,14 +14,14 @@ const FG_MODEL = "\x1b[38;2;202;124;94m";
14
14
  const FG_CYAN = "\x1b[38;5;109m";
15
15
  const FG_WHITE = "\x1b[38;5;255m";
16
16
  const RESET = "\x1b[0m";
17
- function formatTokens(t) {
17
+ export function formatTokens(t) {
18
18
  if (t >= 1_000_000)
19
19
  return (t / 1_000_000).toFixed(1) + "M";
20
20
  if (t >= 1_000)
21
21
  return (t / 1_000).toFixed(1) + "k";
22
22
  return String(t);
23
23
  }
24
- function formatCost(n) {
24
+ export function formatCost(n) {
25
25
  if (n >= 1000)
26
26
  return "$" + Math.round(n).toLocaleString("en-US");
27
27
  if (n >= 100)
@@ -30,13 +30,22 @@ function formatCost(n) {
30
30
  return "$" + n.toFixed(1);
31
31
  return "$" + n.toFixed(2);
32
32
  }
33
- function ctxColor(pct) {
33
+ export function ctxColor(pct) {
34
34
  if (pct >= 80)
35
35
  return FG_RED;
36
36
  if (pct >= 60)
37
37
  return FG_ORANGE;
38
38
  return FG_GREEN;
39
39
  }
40
+ export function formatCountdown(resetsAtMs) {
41
+ const remainingMs = resetsAtMs - Date.now();
42
+ if (remainingMs <= 0)
43
+ return "~0:00";
44
+ const totalMinutes = Math.ceil(remainingMs / 60000);
45
+ const hours = Math.floor(totalMinutes / 60);
46
+ const minutes = totalMinutes % 60;
47
+ return `-${hours}:${String(minutes).padStart(2, "0")}`;
48
+ }
40
49
  // ccclub rank fetcher with 120s file cache
41
50
  function getCcclubRank() {
42
51
  const configPath = join(homedir(), ".ccclub", "config.json");
@@ -77,7 +86,7 @@ function getCcclubRank() {
77
86
  return null;
78
87
  }
79
88
  }
80
- function rankColor(rank) {
89
+ export function rankColor(rank) {
81
90
  if (rank === 1)
82
91
  return FG_YELLOW;
83
92
  if (rank === 2)
@@ -89,10 +98,12 @@ function rankColor(rank) {
89
98
  // Claude usage fetcher with 60s file cache
90
99
  function getClaudeUsage() {
91
100
  const cacheFile = "/tmp/sl-claude-usage";
92
- const now = Date.now() / 1000;
101
+ const hitFile = "/tmp/sl-claude-usage-hit";
102
+ const now = Date.now();
103
+ const nowSec = now / 1000;
93
104
  if (existsSync(cacheFile)) {
94
105
  const mtime = statSync(cacheFile).mtimeMs / 1000;
95
- if (now - mtime <= 60) {
106
+ if (nowSec - mtime <= 60) {
96
107
  try {
97
108
  return JSON.parse(readFileSync(cacheFile, "utf-8"));
98
109
  }
@@ -125,7 +136,42 @@ function getClaudeUsage() {
125
136
  return Math.round(parseFloat(val.replace("%", "")));
126
137
  return 0;
127
138
  };
128
- const result = { fiveHour: parseUtil(data.five_hour?.utilization), sevenDay: parseUtil(data.seven_day?.utilization) };
139
+ const fiveHour = parseUtil(data.five_hour?.utilization);
140
+ const sevenDay = parseUtil(data.seven_day?.utilization);
141
+ let fiveHourResetsAt;
142
+ // Strategy 1: Use reset time from API if available
143
+ const resetsAtRaw = data.five_hour?.resets_at ?? data.five_hour?.reset_at ?? data.five_hour?.next_reset;
144
+ if (resetsAtRaw) {
145
+ const ts = typeof resetsAtRaw === "string" ? new Date(resetsAtRaw).getTime() : resetsAtRaw * 1000;
146
+ if (!isNaN(ts) && ts > now)
147
+ fiveHourResetsAt = ts;
148
+ }
149
+ // Strategy 2: Fallback - track when we first saw 100%
150
+ if (fiveHour >= 100) {
151
+ if (!fiveHourResetsAt) {
152
+ if (existsSync(hitFile)) {
153
+ const hitTime = parseFloat(readFileSync(hitFile, "utf-8").trim());
154
+ if (!isNaN(hitTime)) {
155
+ fiveHourResetsAt = hitTime + 5 * 3600 * 1000;
156
+ }
157
+ }
158
+ else {
159
+ writeFileSync(hitFile, String(now), "utf-8");
160
+ fiveHourResetsAt = now + 5 * 3600 * 1000;
161
+ }
162
+ }
163
+ }
164
+ else {
165
+ // Usage dropped below 100%, clear hit tracker
166
+ try {
167
+ if (existsSync(hitFile))
168
+ unlinkSync(hitFile);
169
+ }
170
+ catch { }
171
+ }
172
+ const result = { fiveHour, sevenDay };
173
+ if (fiveHourResetsAt)
174
+ result.fiveHourResetsAt = fiveHourResetsAt;
129
175
  writeFileSync(cacheFile, JSON.stringify(result), "utf-8");
130
176
  return result;
131
177
  }
@@ -169,24 +215,42 @@ export function render(input) {
169
215
  const cache = readCache();
170
216
  const config = readConfig();
171
217
  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)
218
+ const g = FG_GRAY_DIM;
219
+ const y = FG_YELLOW;
220
+ const m = FG_MODEL;
221
+ const gr = FG_GRAY;
222
+ const r = RESET;
223
+ const cx = ctxColor(contextPct);
224
+ const segments = [];
225
+ // tokens $cost · ctx% Model
226
+ segments.push(`${formatTokens(totalTokens)} ${y}${formatCost(cost)}${r} ${g}·${r} ${cx}${contextPct}%${r} ${m}${model}${r}`);
227
+ // 5h:100% · 7d:26% · 30d:$960
228
+ const usageParts = [];
176
229
  if (claudeUsage) {
177
- parts.push(`${FG_GRAY_DIM}∝${RESET} ${ctxColor(claudeUsage.fiveHour)}5h: ${claudeUsage.fiveHour}%${RESET} ${FG_GRAY_DIM}/${RESET} ${ctxColor(claudeUsage.sevenDay)}7d: ${claudeUsage.sevenDay}%${RESET}`);
230
+ if (claudeUsage.fiveHour >= 100 && claudeUsage.fiveHourResetsAt) {
231
+ const countdown = formatCountdown(claudeUsage.fiveHourResetsAt);
232
+ usageParts.push(`${FG_RED}5h:${countdown}${r}`);
233
+ }
234
+ else {
235
+ const c5 = ctxColor(claudeUsage.fiveHour);
236
+ usageParts.push(`${c5}5h:${claudeUsage.fiveHour}%${r}`);
237
+ }
238
+ const c7 = ctxColor(claudeUsage.sevenDay);
239
+ usageParts.push(`${c7}7d:${claudeUsage.sevenDay}%${r}`);
178
240
  }
179
- // 3. Period cost (default 30d, configurable)
180
241
  if (cache) {
181
242
  const period = config.period || "30d";
182
243
  const periodCost = period === "7d" ? cache.cost7d : cache.cost30d;
183
- parts.push(`${FG_YELLOW}Σ ${period}: ${formatCost(periodCost)}${RESET}`);
244
+ usageParts.push(`${y}${period}:${formatCost(periodCost)}${r}`);
245
+ }
246
+ if (usageParts.length > 0) {
247
+ segments.push(usageParts.join(` ${g}·${r} `));
184
248
  }
185
- // 4. ccclub rank (colored by position)
249
+ // #2 $53.6
186
250
  const ccclubRank = getCcclubRank();
187
251
  if (ccclubRank) {
188
252
  const rc = rankColor(ccclubRank.rank);
189
- parts.push(`${rc}Ω #${ccclubRank.rank}/${ccclubRank.total} ${formatCost(ccclubRank.cost)}${RESET}`);
253
+ segments.push(`${rc}#${ccclubRank.rank} ${formatCost(ccclubRank.cost)}${r}`);
190
254
  }
191
- return "\n " + parts.join(` ${FG_GRAY}|${RESET} `) + "\n";
255
+ return " " + segments.join(` ${gr}/${r} `);
192
256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-costline",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Enhanced statusline for Claude Code with cost tracking, usage limits, and leaderboard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",
11
- "dev": "tsc --watch"
11
+ "dev": "tsc --watch",
12
+ "test": "tsc && node --experimental-strip-types --no-warnings --test test/*.test.ts"
12
13
  },
13
14
  "files": [
14
15
  "dist"