cc-costline 0.3.2 → 0.4.1

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);
@@ -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;
@@ -1,8 +1,11 @@
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
- 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,24 @@ 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
+ export function shouldRefreshLocalCostCache(cache, transcriptPath = "", now = Date.now()) {
53
+ if (!cache)
54
+ return true;
55
+ const cacheUpdatedAt = new Date(cache.updatedAt).getTime();
56
+ if (isNaN(cacheUpdatedAt))
57
+ return true;
58
+ if (transcriptPath) {
59
+ try {
60
+ const transcriptMtime = statSync(transcriptPath).mtimeMs;
61
+ if (transcriptMtime > cacheUpdatedAt)
62
+ return true;
63
+ }
64
+ catch { }
65
+ }
66
+ return now - cacheUpdatedAt >= CACHE_TTL_MS;
67
+ }
68
+ // ccclub rank fetcher — TTL-based cache (stale fallback on failure)
69
+ function getCcclubRank() {
51
70
  const configPath = join(homedir(), ".ccclub", "config.json");
52
71
  if (!existsSync(configPath))
53
72
  return null;
@@ -57,11 +76,8 @@ function getCcclubRank(sessionId) {
57
76
  try {
58
77
  const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
59
78
  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
79
  const cacheAge = Date.now() - (cached.timestamp || 0);
64
- if (cacheAge < 600_000)
80
+ if (cacheAge < CACHE_TTL_MS)
65
81
  return staleData;
66
82
  }
67
83
  catch { }
@@ -83,12 +99,12 @@ function getCcclubRank(sessionId) {
83
99
  if (!me)
84
100
  return staleData;
85
101
  const result = { rank: me.rank, total: rankings.length, cost: me.costUSD };
86
- writeFileSync(cacheFile, JSON.stringify({ sessionId, data: result, timestamp: Date.now() }), "utf-8");
102
+ writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp: Date.now() }), "utf-8");
87
103
  return result;
88
104
  }
89
105
  catch {
90
106
  try {
91
- writeFileSync(cacheFile, JSON.stringify({ sessionId, data: staleData, timestamp: Date.now() }), "utf-8");
107
+ writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: Date.now() }), "utf-8");
92
108
  }
93
109
  catch { }
94
110
  return staleData;
@@ -103,8 +119,8 @@ export function rankColor(rank) {
103
119
  return FG_ORANGE;
104
120
  return FG_CYAN;
105
121
  }
106
- // Claude usage fetcher — cached per session (stale fallback on failure)
107
- function getClaudeUsage(sessionId) {
122
+ // Claude usage fetcher — TTL-based cache (stale fallback on failure)
123
+ function getClaudeUsage() {
108
124
  const cacheFile = "/tmp/sl-claude-usage";
109
125
  const hitFile = "/tmp/sl-claude-usage-hit";
110
126
  const now = Date.now();
@@ -113,11 +129,8 @@ function getClaudeUsage(sessionId) {
113
129
  try {
114
130
  const cached = JSON.parse(readFileSync(cacheFile, "utf-8"));
115
131
  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
132
  const cacheAge = now - (cached.timestamp || 0);
120
- if (cacheAge < 600_000)
133
+ if (cacheAge < CACHE_TTL_MS)
121
134
  return staleData;
122
135
  }
123
136
  catch { }
@@ -140,7 +153,7 @@ function getClaudeUsage(sessionId) {
140
153
  const response = execSync(curlCmd, { encoding: "utf-8", timeout: 5000 });
141
154
  if (!response) {
142
155
  // API failed — write cache with null data to prevent retry flood
143
- writeFileSync(cacheFile, JSON.stringify({ sessionId, data: null, timestamp: now }), "utf-8");
156
+ writeFileSync(cacheFile, JSON.stringify({ data: null, timestamp: now }), "utf-8");
144
157
  return staleData;
145
158
  }
146
159
  const data = JSON.parse(response);
@@ -187,13 +200,13 @@ function getClaudeUsage(sessionId) {
187
200
  const result = { fiveHour, sevenDay };
188
201
  if (fiveHourResetsAt)
189
202
  result.fiveHourResetsAt = fiveHourResetsAt;
190
- writeFileSync(cacheFile, JSON.stringify({ sessionId, data: result, timestamp: now }), "utf-8");
203
+ writeFileSync(cacheFile, JSON.stringify({ data: result, timestamp: now }), "utf-8");
191
204
  return result;
192
205
  }
193
206
  catch {
194
207
  // Write cache with stale/null data to prevent retry flood on persistent failures
195
208
  try {
196
- writeFileSync(cacheFile, JSON.stringify({ sessionId, data: staleData, timestamp: now }), "utf-8");
209
+ writeFileSync(cacheFile, JSON.stringify({ data: staleData, timestamp: now }), "utf-8");
197
210
  }
198
211
  catch { }
199
212
  return staleData;
@@ -211,9 +224,7 @@ export function render(input) {
211
224
  const cost = data.cost?.total_cost_usd ?? 0;
212
225
  const model = data.model?.display_name ?? "—";
213
226
  const contextPct = Math.floor(data.context_window?.used_percentage ?? 0);
214
- // Session ID from transcript path (filename without extension)
215
227
  const transcriptPath = data.transcript_path ?? "";
216
- const sessionId = transcriptPath ? transcriptPath.replace(/^.*\//, "").replace(/\.jsonl$/, "") : "";
217
228
  // Token stats from transcript
218
229
  let totalTokens = 0;
219
230
  if (transcriptPath) {
@@ -234,9 +245,22 @@ export function render(input) {
234
245
  }
235
246
  catch { }
236
247
  }
237
- const cache = readCache();
248
+ // Refresh local cost cache if stale
249
+ let cache = readCache();
250
+ if (shouldRefreshLocalCostCache(cache, transcriptPath)) {
251
+ try {
252
+ const result = collectCosts();
253
+ // Don't overwrite valid cache with zeros (directory read failure)
254
+ if (result.cost7d > 0 || result.cost30d > 0 || !cache) {
255
+ const newCache = { cost7d: result.cost7d, cost30d: result.cost30d, updatedAt: new Date().toISOString() };
256
+ writeCache(newCache);
257
+ cache = newCache;
258
+ }
259
+ }
260
+ catch { }
261
+ }
238
262
  const config = readConfig();
239
- const claudeUsage = getClaudeUsage(sessionId);
263
+ const claudeUsage = getClaudeUsage();
240
264
  const g = FG_GRAY_DIM;
241
265
  const y = FG_YELLOW;
242
266
  const m = FG_MODEL;
@@ -262,14 +286,20 @@ export function render(input) {
262
286
  }
263
287
  if (cache) {
264
288
  const period = config.period || "30d";
265
- const periodCost = period === "7d" ? cache.cost7d : cache.cost30d;
266
- usageParts.push(`${y}${period}:${formatCost(periodCost)}${r}`);
289
+ if (period === "both") {
290
+ usageParts.push(`${y}7d:${formatCost(cache.cost7d)}${r}`);
291
+ usageParts.push(`${y}30d:${formatCost(cache.cost30d)}${r}`);
292
+ }
293
+ else {
294
+ const periodCost = period === "7d" ? cache.cost7d : cache.cost30d;
295
+ usageParts.push(`${y}${period}:${formatCost(periodCost)}${r}`);
296
+ }
267
297
  }
268
298
  if (usageParts.length > 0) {
269
299
  segments.push(usageParts.join(` ${g}·${r} `));
270
300
  }
271
301
  // #2 $53.6
272
- const ccclubRank = getCcclubRank(sessionId);
302
+ const ccclubRank = getCcclubRank();
273
303
  if (ccclubRank) {
274
304
  const rc = rankColor(ccclubRank.rank);
275
305
  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.2",
3
+ "version": "0.4.1",
4
4
  "description": "Enhanced statusline for Claude Code with cost tracking, usage limits, and leaderboard",
5
5
  "type": "module",
6
6
  "bin": {