cc-costline 0.3.1 → 0.4.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
@@ -24,13 +24,13 @@ Abre una nueva sesión de Claude Code y verás la statusline mejorada. Requiere
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
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
- | Costo del período | `30d: $866` | Costo acumulado (configurable: 7d o 30d) |
27
+ | Costo del período | `30d: $866` | Costo acumulado (configurable: 7d, 30d o both) |
28
28
  | Ranking | `#2/22 $67.0` | Posición en [ccclub](https://github.com/mazzzystar/ccclub) (si está instalado) |
29
29
 
30
30
  ### Colores
31
31
 
32
32
  - **Contexto y límites de uso** — verde (< 60%) → naranja (60-79%) → rojo (≥ 80%)
33
- - **Posición en ranking** — 1.o: dorado, 2.o: blanco, 3.o: naranja, resto: azul
33
+ - **Posición en ranking** — 1.o: dorado, 2.o: blanco, 3.o: naranja, resto: cian
34
34
  - **Costo del período** — amarillo
35
35
 
36
36
  ### Integraciones opcionales
@@ -46,17 +46,19 @@ Ambas funcionan sin configuración: si no están disponibles, el segmento se ocu
46
46
  cc-costline install # Configurar la integración con Claude Code
47
47
  cc-costline uninstall # Eliminar de la configuración
48
48
  cc-costline refresh # Recalcular manualmente la caché de costos
49
- cc-costline config --period 30d # Mostrar costo de 30 días (por defecto)
50
- cc-costline config --period 7d # Mostrar costo de 7 días
49
+ cc-costline config --period 7d # Mostrar costo de 7 días (por defecto)
50
+ cc-costline config --period 30d # Mostrar costo de 30 días
51
+ cc-costline config --period both # Mostrar ambos períodos
51
52
  ```
52
53
 
53
54
  ## Cómo funciona
54
55
 
55
- 1. `install` configura `~/.claude/settings.json` — establece el comando de statusline y añade hooks de fin de sesión para la actualización automática. Tu configuración existente se conserva.
56
- 2. `render` lee el JSON de stdin de Claude Code y la caché de costos, y genera la statusline formateada.
57
- 3. `refresh` escanea `~/.claude/projects/**/*.jsonl`, extrae el uso de tokens, aplica precios por modelo y escribe en `~/.cc-costline/cache.json`.
58
- 4. El uso de Claude se obtiene de `api.anthropic.com/api/oauth/usage` con una caché de 60 s en `/tmp/sl-claude-usage`.
59
- 5. El ranking de ccclub se obtiene de `ccclub.dev/api/rank` con una caché de 120 s en `/tmp/sl-ccclub-rank`.
56
+ 1. `install` configura `~/.claude/settings.json` — establece el comando de statusline y añade hooks de fin de sesión. Tu configuración existente se conserva.
57
+ 2. `render` es llamado por Claude Code en cada turno de conversación. Lee el JSON de stdin para datos de sesión, luego actualiza todas las fuentes de datos en línea cuando su caché expira (TTL unificado de 2 minutos):
58
+ - **Costo local**: escanea `~/.claude/projects/**/*.jsonl`, aplica precios por modelo `~/.cc-costline/cache.json`
59
+ - **Límites de uso**: obtiene de `api.anthropic.com/api/oauth/usage` `/tmp/sl-claude-usage`
60
+ - **Ranking ccclub**: obtiene de `ccclub.dev/api/rank` `/tmp/sl-ccclub-rank`
61
+ 3. `refresh` también puede ejecutarse manualmente o mediante hooks de fin de sesión para precalentar la caché.
60
62
 
61
63
  <details>
62
64
  <summary>Tabla de precios</summary>
package/README.fr.md CHANGED
@@ -24,13 +24,13 @@ Ouvrez une nouvelle session Claude Code et la statusline enrichie apparaîtra. N
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
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
- | Coût périodique | `30d: $866` | Coût cumulé glissant (configurable : 7j ou 30j) |
27
+ | Coût périodique | `30d: $866` | Coût cumulé glissant (configurable : 7j, 30j ou both) |
28
28
  | Classement | `#2/22 $67.0` | Rang [ccclub](https://github.com/mazzzystar/ccclub) (si installé) |
29
29
 
30
30
  ### Couleurs
31
31
 
32
32
  - **Contexte et limites** — vert (< 60 %) → orange (60-79 %) → rouge (≥ 80 %)
33
- - **Rang au classement** — 1er : or, 2e : blanc, 3e : orange, autres : bleu
33
+ - **Rang au classement** — 1er : or, 2e : blanc, 3e : orange, autres : cyan
34
34
  - **Coût périodique** — jaune
35
35
 
36
36
  ### Intégrations optionnelles
@@ -46,17 +46,19 @@ Les deux fonctionnent sans configuration : si indisponibles, le segment est masq
46
46
  cc-costline install # Configurer l'intégration Claude Code
47
47
  cc-costline uninstall # Supprimer des paramètres
48
48
  cc-costline refresh # Recalculer manuellement le cache des coûts
49
- cc-costline config --period 30d # Afficher le coût sur 30 jours (par défaut)
50
- cc-costline config --period 7d # Afficher le coût sur 7 jours
49
+ cc-costline config --period 7d # Afficher le coût sur 7 jours (par défaut)
50
+ cc-costline config --period 30d # Afficher le coût sur 30 jours
51
+ cc-costline config --period both # Afficher les deux périodes
51
52
  ```
52
53
 
53
54
  ## Fonctionnement
54
55
 
55
- 1. `install` configure `~/.claude/settings.json` — définit la commande statusline et ajoute des hooks de fin de session pour le rafraîchissement automatique. Vos paramètres existants sont préservés.
56
- 2. `render` lit le JSON stdin de Claude Code et le cache des coûts, puis produit la statusline formatée.
57
- 3. `refresh` parcourt `~/.claude/projects/**/*.jsonl`, extrait l'utilisation des tokens, applique la tarification par modèle et écrit dans `~/.cc-costline/cache.json`.
58
- 4. L'utilisation Claude est récupérée depuis `api.anthropic.com/api/oauth/usage` avec un cache fichier de 60 s dans `/tmp/sl-claude-usage`.
59
- 5. Le rang ccclub est récupéré depuis `ccclub.dev/api/rank` avec un cache fichier de 120 s dans `/tmp/sl-ccclub-rank`.
56
+ 1. `install` configure `~/.claude/settings.json` — définit la commande statusline et ajoute des hooks de fin de session. Vos paramètres existants sont préservés.
57
+ 2. `render` est appelé par Claude Code à chaque tour de conversation. Il lit le JSON stdin pour les données de session, puis rafraîchit toutes les sources de données en ligne lorsque leur cache expire (TTL unifié de 2 minutes) :
58
+ - **Coût local** : parcourt `~/.claude/projects/**/*.jsonl`, applique la tarification par modèle `~/.cc-costline/cache.json`
59
+ - **Limites d'utilisation** : récupère depuis `api.anthropic.com/api/oauth/usage` `/tmp/sl-claude-usage`
60
+ - **Rang ccclub** : récupère depuis `ccclub.dev/api/rank` `/tmp/sl-ccclub-rank`
61
+ 3. `refresh` peut aussi être exécuté manuellement ou via les hooks de fin de session pour préchauffer le cache.
60
62
 
61
63
  <details>
62
64
  <summary>Grille tarifaire</summary>
package/README.ja.md CHANGED
@@ -24,13 +24,13 @@ npm i -g cc-costline && cc-costline install
24
24
  |-----------|---|------|
25
25
  | トークン ~ コスト / コンテキスト | `14.6k ~ $2.42 / 40% by Opus 4.6` | セッションのトークン数、コスト、コンテキスト使用率、モデル |
26
26
  | 使用制限 | `5h: 45% / 7d: 8%` | Claude の 5 時間・7 日間の使用率(コンテキストと同じ色分け)。100% 到達時はカウントダウン表示:`5h:-3:20` |
27
- | 期間コスト | `30d: $866` | ローリングコスト合計(7d または 30d で設定可能) |
27
+ | 期間コスト | `30d: $866` | ローリングコスト合計(7d、30d、または both で設定可能) |
28
28
  | リーダーボード | `#2/22 $67.0` | [ccclub](https://github.com/mazzzystar/ccclub) ランキング(インストール時) |
29
29
 
30
30
  ### カラールール
31
31
 
32
32
  - **コンテキスト・使用制限** — 緑(< 60%)→ オレンジ(60-79%)→ 赤(≥ 80%)
33
- - **リーダーボードランク** — 1 位:ゴールド、2 位:ホワイト、3 位:オレンジ、その他:ブルー
33
+ - **リーダーボードランク** — 1 位:ゴールド、2 位:ホワイト、3 位:オレンジ、その他:シアン
34
34
  - **期間コスト** — イエロー
35
35
 
36
36
  ### オプション連携
@@ -46,17 +46,19 @@ npm i -g cc-costline && cc-costline install
46
46
  cc-costline install # Claude Code 連携のセットアップ
47
47
  cc-costline uninstall # 設定から削除
48
48
  cc-costline refresh # コストキャッシュを手動再計算
49
- cc-costline config --period 30d # 30 日間のコストを表示(デフォルト)
50
- cc-costline config --period 7d # 7 日間のコストを表示
49
+ cc-costline config --period 7d # 7 日間のコストを表示(デフォルト)
50
+ cc-costline config --period 30d # 30 日間のコストを表示
51
+ cc-costline config --period both # 両方の期間を表示
51
52
  ```
52
53
 
53
54
  ## 仕組み
54
55
 
55
56
  1. `install` は `~/.claude/settings.json` を設定 — ステータスラインコマンドとセッション終了フックを追加します。既存の設定は保持されます。
56
- 2. `render` Claude Code stdin JSON とコストキャッシュを読み取り、フォーマットされたステータスラインを出力します。
57
- 3. `refresh` は `~/.claude/projects/**/*.jsonl` をスキャンし、トークン使用量を抽出、モデル別価格を適用して `~/.cc-costline/cache.json` に書き込みます。
58
- 4. Claude 使用率は `api.anthropic.com/api/oauth/usage` から取得され、60 秒のファイルキャッシュが `/tmp/sl-claude-usage` に保存されます。
59
- 5. ccclub ランキングは `ccclub.dev/api/rank` から取得され、120 秒のファイルキャッシュが `/tmp/sl-ccclub-rank` に保存されます。
57
+ 2. `render` は毎回の対話時に Claude Code から呼び出され、stdin JSON からセッションデータを読み取り、すべてのデータソースをキャッシュ期限切れ時にインラインで更新します(統一 2 分 TTL):
58
+ - **ローカルコスト**:`~/.claude/projects/**/*.jsonl` をスキャン、モデル別価格を適用 `~/.cc-costline/cache.json`
59
+ - **使用率**:`api.anthropic.com/api/oauth/usage` から取得 `/tmp/sl-claude-usage`
60
+ - **ccclub ランキング**:`ccclub.dev/api/rank` から取得 `/tmp/sl-ccclub-rank`
61
+ 3. `refresh` は手動実行やセッション終了フックによるキャッシュウォームアップにも使用できます。
60
62
 
61
63
  <details>
62
64
  <summary>料金表</summary>
package/README.md CHANGED
@@ -24,13 +24,13 @@ Open a new Claude Code session and you'll see the enhanced statusline. Requires
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
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
- | Period cost | `30d: $866` | Rolling cost total (configurable: 7d or 30d) |
27
+ | Period cost | `30d: $866` | Rolling cost total (configurable: 7d, 30d, or both) |
28
28
  | Leaderboard | `#2/22 $67.0` | [ccclub](https://github.com/mazzzystar/ccclub) rank (if installed) |
29
29
 
30
30
  ### Colors
31
31
 
32
32
  - **Context & usage limits** — green (< 60%) → orange (60-79%) → red (≥ 80%)
33
- - **Leaderboard rank** — #1 gold, #2 white, #3 orange, others blue
33
+ - **Leaderboard rank** — #1 gold, #2 white, #3 orange, others cyan
34
34
  - **Period cost** — yellow
35
35
 
36
36
  ### Optional integrations
@@ -46,17 +46,19 @@ Both are zero-config: if not available, the segment is silently omitted.
46
46
  cc-costline install # Set up Claude Code integration
47
47
  cc-costline uninstall # Remove from settings
48
48
  cc-costline refresh # Manually recalculate cost cache
49
- cc-costline config --period 30d # Show 30-day cost (default)
50
- cc-costline config --period 7d # Show 7-day cost
49
+ cc-costline config --period 7d # Show 7-day cost (default)
50
+ cc-costline config --period 30d # Show 30-day cost
51
+ cc-costline config --period both # Show both periods
51
52
  ```
52
53
 
53
54
  ## How it works
54
55
 
55
- 1. `install` configures `~/.claude/settings.json` — sets the statusline command and adds session-end hooks for auto-refresh. Your existing settings are preserved.
56
- 2. `render` reads Claude Code's stdin JSON and the cost cache, outputs the formatted statusline.
57
- 3. `refresh` scans `~/.claude/projects/**/*.jsonl`, extracts token usage, applies per-model pricing, and writes to `~/.cc-costline/cache.json`.
58
- 4. Claude usage is fetched from `api.anthropic.com/api/oauth/usage` with a 60s file cache at `/tmp/sl-claude-usage`.
59
- 5. ccclub rank is fetched from `ccclub.dev/api/rank` with a 120s file cache at `/tmp/sl-ccclub-rank`.
56
+ 1. `install` configures `~/.claude/settings.json` — sets the statusline command and adds session-end hooks. Your existing settings are preserved.
57
+ 2. `render` is called by Claude Code on every turn. It reads stdin JSON for session data, then refreshes all data sources inline when their cache expires (2-minute TTL):
58
+ - **Local cost**: scans `~/.claude/projects/**/*.jsonl`, applies per-model pricing `~/.cc-costline/cache.json`
59
+ - **Usage limits**: fetches `api.anthropic.com/api/oauth/usage` `/tmp/sl-claude-usage`
60
+ - **ccclub rank**: fetches `ccclub.dev/api/rank` `/tmp/sl-ccclub-rank`
61
+ 3. `refresh` can also be run manually or via session-end hooks to warm the cost cache.
60
62
 
61
63
  <details>
62
64
  <summary>Pricing table</summary>
package/README.zh-CN.md CHANGED
@@ -24,13 +24,13 @@ npm i -g cc-costline && cc-costline install
24
24
  |------|------|------|
25
25
  | Token ~ 费用 / 上下文 | `14.6k ~ $2.42 / 40% by Opus 4.6` | 会话 token 数量、费用、上下文使用率和模型 |
26
26
  | 使用限额 | `5h: 45% / 7d: 8%` | Claude 5 小时和 7 天使用率(颜色同上下文)。达到 100% 时显示倒计时:`5h:-3:20` |
27
- | 周期费用 | `30d: $866` | 滚动费用合计(可配置:7d 或 30d) |
27
+ | 周期费用 | `30d: $866` | 滚动费用合计(可配置:7d、30dboth) |
28
28
  | 排行榜 | `#2/22 $67.0` | [ccclub](https://github.com/mazzzystar/ccclub) 排名(需安装) |
29
29
 
30
30
  ### 颜色规则
31
31
 
32
32
  - **上下文和使用限额** — 绿色(< 60%)→ 橙色(60-79%)→ 红色(≥ 80%)
33
- - **排行榜排名** — 第 1 名金色,第 2 名白色,第 3 名橙色,其余蓝色
33
+ - **排行榜排名** — 第 1 名金色,第 2 名白色,第 3 名橙色,其余青色
34
34
  - **周期费用** — 黄色
35
35
 
36
36
  ### 可选集成
@@ -46,17 +46,19 @@ npm i -g cc-costline && cc-costline install
46
46
  cc-costline install # 设置 Claude Code 集成
47
47
  cc-costline uninstall # 从设置中移除
48
48
  cc-costline refresh # 手动重新计算费用缓存
49
- cc-costline config --period 30d # 显示 30 天费用(默认)
50
- cc-costline config --period 7d # 显示 7 天费用
49
+ cc-costline config --period 7d # 显示 7 天费用(默认)
50
+ cc-costline config --period 30d # 显示 30 天费用
51
+ cc-costline config --period both # 同时显示两个周期
51
52
  ```
52
53
 
53
54
  ## 工作原理
54
55
 
55
- 1. `install` 配置 `~/.claude/settings.json` — 设置状态栏命令并添加会话结束 hook 以自动刷新。你的现有设置会被保留。
56
- 2. `render` 读取 Claude Code stdin JSON 和费用缓存,输出格式化的状态栏。
57
- 3. `refresh` 扫描 `~/.claude/projects/**/*.jsonl`,提取 token 用量,按模型定价计算,写入 `~/.cc-costline/cache.json`。
58
- 4. Claude 使用率从 `api.anthropic.com/api/oauth/usage` 获取,60 秒文件缓存于 `/tmp/sl-claude-usage`。
59
- 5. ccclub 排名从 `ccclub.dev/api/rank` 获取,120 秒文件缓存于 `/tmp/sl-ccclub-rank`。
56
+ 1. `install` 配置 `~/.claude/settings.json` — 设置状态栏命令并添加会话结束 hook。你的现有设置会被保留。
57
+ 2. `render` 在每次对话时被 Claude Code 调用,读取 stdin JSON 获取会话数据,然后按需刷新所有数据源(统一 2 分钟 TTL):
58
+ - **本地费用**:扫描 `~/.claude/projects/**/*.jsonl`,按模型定价计算 `~/.cc-costline/cache.json`
59
+ - **使用率**:从 `api.anthropic.com/api/oauth/usage` 获取 `/tmp/sl-claude-usage`
60
+ - **ccclub 排名**:从 `ccclub.dev/api/rank` 获取 `/tmp/sl-ccclub-rank`
61
+ 3. `refresh` 也可以手动运行或通过会话结束 hook 预热缓存。
60
62
 
61
63
  <details>
62
64
  <summary>定价表</summary>
package/dist/cache.d.ts CHANGED
@@ -7,8 +7,8 @@ export interface CacheData {
7
7
  export interface ConfigData {
8
8
  period: "7d" | "30d" | "both";
9
9
  }
10
- export declare function readCache(): CacheData | null;
11
- export declare function writeCache(data: CacheData): void;
12
- export declare function readConfig(): ConfigData;
13
- export declare function writeConfig(data: ConfigData): void;
10
+ export declare function readCache(dir?: string): CacheData | null;
11
+ export declare function writeCache(data: CacheData, dir?: string): void;
12
+ export declare function readConfig(dir?: string): ConfigData;
13
+ export declare function writeConfig(data: ConfigData, dir?: string): void;
14
14
  export { CACHE_DIR };
package/dist/cache.js CHANGED
@@ -2,32 +2,32 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  const CACHE_DIR = join(homedir(), ".cc-costline");
5
- const CACHE_FILE = join(CACHE_DIR, "cache.json");
6
- const CONFIG_FILE = join(CACHE_DIR, "config.json");
7
- export function readCache() {
5
+ export function readCache(dir) {
8
6
  try {
9
- const raw = readFileSync(CACHE_FILE, "utf-8");
7
+ const raw = readFileSync(join(dir || CACHE_DIR, "cache.json"), "utf-8");
10
8
  return JSON.parse(raw);
11
9
  }
12
10
  catch {
13
11
  return null;
14
12
  }
15
13
  }
16
- export function writeCache(data) {
17
- mkdirSync(CACHE_DIR, { recursive: true });
18
- writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2) + "\n");
14
+ export function writeCache(data, dir) {
15
+ const d = dir || CACHE_DIR;
16
+ mkdirSync(d, { recursive: true });
17
+ writeFileSync(join(d, "cache.json"), JSON.stringify(data, null, 2) + "\n");
19
18
  }
20
- export function readConfig() {
19
+ export function readConfig(dir) {
21
20
  try {
22
- const raw = readFileSync(CONFIG_FILE, "utf-8");
21
+ const raw = readFileSync(join(dir || CACHE_DIR, "config.json"), "utf-8");
23
22
  return JSON.parse(raw);
24
23
  }
25
24
  catch {
26
25
  return { period: "7d" };
27
26
  }
28
27
  }
29
- export function writeConfig(data) {
30
- mkdirSync(CACHE_DIR, { recursive: true });
31
- writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2) + "\n");
28
+ export function writeConfig(data, dir) {
29
+ const d = dir || CACHE_DIR;
30
+ mkdirSync(d, { recursive: true });
31
+ writeFileSync(join(d, "config.json"), JSON.stringify(data, null, 2) + "\n");
32
32
  }
33
33
  export { CACHE_DIR };
package/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
3
- import { join } from "node:path";
3
+ import { join, dirname } from "node:path";
4
4
  import { homedir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
5
8
  import { collectCosts } from "./collector.js";
6
9
  import { writeCache, writeConfig, readConfig, CACHE_DIR } from "./cache.js";
7
10
  import { render } from "./statusline.js";
@@ -10,11 +13,16 @@ const RENDER_COMMAND = "cc-costline render";
10
13
  const REFRESH_COMMAND = "cc-costline refresh";
11
14
  // ─── Helpers ──────────────────────────────────────────────
12
15
  function readSettings() {
16
+ if (!existsSync(SETTINGS_PATH))
17
+ return {};
18
+ const raw = readFileSync(SETTINGS_PATH, "utf-8");
13
19
  try {
14
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
20
+ return JSON.parse(raw);
15
21
  }
16
22
  catch {
17
- return {};
23
+ console.error(`✗ Failed to parse ${SETTINGS_PATH} — aborting to avoid overwriting your config.`);
24
+ console.error(" Please fix the JSON syntax and retry.");
25
+ process.exit(1);
18
26
  }
19
27
  }
20
28
  function saveSettings(settings) {
@@ -143,7 +151,7 @@ switch (command) {
143
151
  cmdRender();
144
152
  break;
145
153
  default:
146
- console.log(`cc-costline v0.1.0 — Enhanced statusline for Claude Code
154
+ console.log(`cc-costline v${pkg.version} — Enhanced statusline for Claude Code
147
155
 
148
156
  Commands:
149
157
  install Configure Claude Code to use cc-costline
@@ -2,5 +2,5 @@ interface CollectResult {
2
2
  cost7d: number;
3
3
  cost30d: number;
4
4
  }
5
- export declare function collectCosts(): CollectResult;
5
+ export declare function collectCosts(baseDir?: string): CollectResult;
6
6
  export {};
package/dist/collector.js CHANGED
@@ -31,8 +31,8 @@ function findJsonlFiles(dir) {
31
31
  }
32
32
  return results;
33
33
  }
34
- export function collectCosts() {
35
- const projectsDir = join(homedir(), CLAUDE_PROJECTS_DIR);
34
+ export function collectCosts(baseDir) {
35
+ const projectsDir = baseDir || join(homedir(), CLAUDE_PROJECTS_DIR);
36
36
  const files = findJsonlFiles(projectsDir);
37
37
  if (files.length === 0) {
38
38
  return { cost7d: 0, cost30d: 0 };
@@ -73,7 +73,7 @@ export function collectCosts() {
73
73
  const sessionId = parsed.sessionId || "";
74
74
  const dedupeKey = requestId
75
75
  ? `${sessionId}:${requestId}`
76
- : `${sessionId}:${parsed.timestamp}:${usage.input_tokens}:${usage.output_tokens}`;
76
+ : `${sessionId}:${parsed.timestamp}:${parsed.message.model}:${usage.input_tokens}:${usage.output_tokens}:${usage.cache_creation_input_tokens || 0}:${usage.cache_read_input_tokens || 0}`;
77
77
  if (seen.has(dedupeKey))
78
78
  continue;
79
79
  seen.add(dedupeKey);
@@ -2,7 +2,10 @@ 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";
5
- import { readCache, readConfig } from "./cache.js";
5
+ import { readCache, writeCache, readConfig } from "./cache.js";
6
+ import { collectCosts } from "./collector.js";
7
+ // Unified TTL for all cached data (2 minutes)
8
+ const CACHE_TTL_MS = 120_000;
6
9
  // ANSI colors (matching original statusline.sh)
7
10
  const FG_GRAY = "\x1b[38;5;245m";
8
11
  const FG_GRAY_DIM = "\x1b[38;5;102m";
@@ -46,8 +49,8 @@ export function formatCountdown(resetsAtMs) {
46
49
  const minutes = totalMinutes % 60;
47
50
  return `-${hours}:${String(minutes).padStart(2, "0")}`;
48
51
  }
49
- // ccclub rank fetcher — cached per session (stale fallback on failure)
50
- function getCcclubRank(sessionId) {
52
+ // ccclub rank fetcher — TTL-based cache (stale fallback on failure)
53
+ function getCcclubRank() {
51
54
  const configPath = join(homedir(), ".ccclub", "config.json");
52
55
  if (!existsSync(configPath))
53
56
  return null;
@@ -57,7 +60,8 @@ function getCcclubRank(sessionId) {
57
60
  try {
58
61
  const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
59
62
  staleData = cached.data ?? null;
60
- if (cached.sessionId === sessionId)
63
+ const cacheAge = Date.now() - (cached.timestamp || 0);
64
+ if (cacheAge < CACHE_TTL_MS)
61
65
  return staleData;
62
66
  }
63
67
  catch { }
@@ -79,10 +83,14 @@ function getCcclubRank(sessionId) {
79
83
  if (!me)
80
84
  return staleData;
81
85
  const result = { rank: me.rank, total: rankings.length, cost: me.costUSD };
82
- writeFileSync(cacheFile, JSON.stringify({ sessionId, data: result }), "utf-8");
86
+ writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp: Date.now() }), "utf-8");
83
87
  return result;
84
88
  }
85
89
  catch {
90
+ try {
91
+ writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: Date.now() }), "utf-8");
92
+ }
93
+ catch { }
86
94
  return staleData;
87
95
  }
88
96
  }
@@ -95,8 +103,8 @@ export function rankColor(rank) {
95
103
  return FG_ORANGE;
96
104
  return FG_CYAN;
97
105
  }
98
- // Claude usage fetcher — cached per session (stale fallback on failure)
99
- function getClaudeUsage(sessionId) {
106
+ // Claude usage fetcher — TTL-based cache (stale fallback on failure)
107
+ function getClaudeUsage() {
100
108
  const cacheFile = "/tmp/sl-claude-usage";
101
109
  const hitFile = "/tmp/sl-claude-usage-hit";
102
110
  const now = Date.now();
@@ -105,7 +113,8 @@ function getClaudeUsage(sessionId) {
105
113
  try {
106
114
  const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
107
115
  staleData = cached.data ?? null;
108
- if (cached.sessionId === sessionId)
116
+ const cacheAge = now - (cached.timestamp || 0);
117
+ if (cacheAge < CACHE_TTL_MS)
109
118
  return staleData;
110
119
  }
111
120
  catch { }
@@ -124,10 +133,13 @@ function getClaudeUsage(sessionId) {
124
133
  if (expiresAt && Date.now() / 1000 > expiresAt)
125
134
  return null;
126
135
  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"`;
136
+ const curlCmd = `curl -sf "${apiUrl}" -H "Authorization: Bearer ${accessToken}" -H "anthropic-beta: oauth-2025-04-20"`;
128
137
  const response = execSync(curlCmd, { encoding: "utf-8", timeout: 5000 });
129
- if (!response)
130
- return null;
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");
141
+ return staleData;
142
+ }
131
143
  const data = JSON.parse(response);
132
144
  const parseUtil = (val) => {
133
145
  if (typeof val === "number")
@@ -172,10 +184,15 @@ function getClaudeUsage(sessionId) {
172
184
  const result = { fiveHour, sevenDay };
173
185
  if (fiveHourResetsAt)
174
186
  result.fiveHourResetsAt = fiveHourResetsAt;
175
- writeFileSync(cacheFile, JSON.stringify({ sessionId, data: result }), "utf-8");
187
+ writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp: now }), "utf-8");
176
188
  return result;
177
189
  }
178
190
  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 { }
179
196
  return staleData;
180
197
  }
181
198
  }
@@ -191,9 +208,7 @@ export function render(input) {
191
208
  const cost = data.cost?.total_cost_usd ?? 0;
192
209
  const model = data.model?.display_name ?? "—";
193
210
  const contextPct = Math.floor(data.context_window?.used_percentage ?? 0);
194
- // Session ID from transcript path (filename without extension)
195
211
  const transcriptPath = data.transcript_path ?? "";
196
- const sessionId = transcriptPath ? transcriptPath.replace(/^.*\//, "").replace(/\.jsonl$/, "") : "";
197
212
  // Token stats from transcript
198
213
  let totalTokens = 0;
199
214
  if (transcriptPath) {
@@ -214,9 +229,23 @@ export function render(input) {
214
229
  }
215
230
  catch { }
216
231
  }
217
- const cache = readCache();
232
+ // Refresh local cost cache if stale
233
+ let cache = readCache();
234
+ const cacheAge = cache ? Date.now() - new Date(cache.updatedAt).getTime() : Infinity;
235
+ if (cacheAge >= CACHE_TTL_MS) {
236
+ try {
237
+ const result = collectCosts();
238
+ // Don't overwrite valid cache with zeros (directory read failure)
239
+ if (result.cost7d > 0 || result.cost30d > 0 || !cache) {
240
+ const newCache = { cost7d: result.cost7d, cost30d: result.cost30d, updatedAt: new Date().toISOString() };
241
+ writeCache(newCache);
242
+ cache = newCache;
243
+ }
244
+ }
245
+ catch { }
246
+ }
218
247
  const config = readConfig();
219
- const claudeUsage = getClaudeUsage(sessionId);
248
+ const claudeUsage = getClaudeUsage();
220
249
  const g = FG_GRAY_DIM;
221
250
  const y = FG_YELLOW;
222
251
  const m = FG_MODEL;
@@ -242,14 +271,20 @@ export function render(input) {
242
271
  }
243
272
  if (cache) {
244
273
  const period = config.period || "30d";
245
- const periodCost = period === "7d" ? cache.cost7d : cache.cost30d;
246
- usageParts.push(`${y}${period}:${formatCost(periodCost)}${r}`);
274
+ if (period === "both") {
275
+ usageParts.push(`${y}7d:${formatCost(cache.cost7d)}${r}`);
276
+ usageParts.push(`${y}30d:${formatCost(cache.cost30d)}${r}`);
277
+ }
278
+ else {
279
+ const periodCost = period === "7d" ? cache.cost7d : cache.cost30d;
280
+ usageParts.push(`${y}${period}:${formatCost(periodCost)}${r}`);
281
+ }
247
282
  }
248
283
  if (usageParts.length > 0) {
249
284
  segments.push(usageParts.join(` ${g}·${r} `));
250
285
  }
251
286
  // #2 $53.6
252
- const ccclubRank = getCcclubRank(sessionId);
287
+ const ccclubRank = getCcclubRank();
253
288
  if (ccclubRank) {
254
289
  const rc = rankColor(ccclubRank.rank);
255
290
  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.1",
3
+ "version": "0.4.0",
4
4
  "description": "Enhanced statusline for Claude Code with cost tracking, usage limits, and leaderboard",
5
5
  "type": "module",
6
6
  "bin": {