cc-usage-bar 0.1.0 → 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/AGENTS.md ADDED
@@ -0,0 +1,233 @@
1
+ # AGENTS.md — cc-usage-bar 全功能说明
2
+
3
+ > 这份文件是面向 AI 助手(Claude Code、Cursor、Codex 等)的安装/配置说明。如果用户对你说「装 cc-usage-bar」「帮我配 statusline 用量条」「装下 cc-usage-bar 帮我配好」之类的话,请按本文档完成全部步骤。**默认无需向用户追加确认**;只有用户没登录过 Claude Code(凭证查不到)时才停下来让用户先登录。
4
+
5
+ ## 1. 这个包是什么
6
+
7
+ `cc-usage-bar` 把 Claude Code 订阅用量(5 小时滑窗 + 7 天滑窗)渲染到 statusline。也支持余额型 provider(Kimi / GLM / MiniMax / DeepSeek / StepFun / SiliconFlow / OpenRouter / Novita)。
8
+
9
+ **核心 bin**:
10
+
11
+ | bin | 用途 |
12
+ |---|---|
13
+ | `cc-usage-fetch` | 单纯渲染一段用量字符串到 stdout(最常用) |
14
+ | `cc-usage-bar-wrap` | 跨平台编排器:tee stdin 给「前缀命令」+ `cc-usage-fetch`,按真实终端宽度自动单行/换行 |
15
+ | `cc-usage-bar` / `cc-usage-statusline` | 安装器(自动改 settings.json),带 `agents`/`install`/`uninstall`/`status` 子命令 |
16
+
17
+ ## 2. 一句话目标
18
+
19
+ > **用户只描述偏好,你把 `statusLine.command` 配成 `cc-usage-bar-wrap`,并保留用户原有前缀。**
20
+
21
+ 如果已有 `statusLine.command`、`statusline-command.sh`、自定义 shell 脚本、git/path/model 展示等前缀内容,**一律视为要保留的前缀**:按 3.4 抽出它,并用 `cc-usage-bar-wrap --prefix=...` 串起来。不要询问是否覆盖、追加或暂不修改;追加保留是本项目默认行为。
22
+
23
+ ## 3. 安装流程(必走)
24
+
25
+ 按顺序执行,每一步执行前简要告知用户:
26
+
27
+ ### 3.1 判断平台
28
+
29
+ ```js
30
+ node -e "console.log(process.platform)"
31
+ // → "darwin" | "linux" | "win32"
32
+ ```
33
+
34
+ ### 3.2 安装 npm 包
35
+
36
+ ```sh
37
+ npm i -g cc-usage-bar
38
+ # 验证(macOS/Linux)
39
+ which cc-usage-bar-wrap && which cc-usage-fetch
40
+ # 验证(Windows)
41
+ where.exe cc-usage-bar-wrap
42
+ ```
43
+
44
+ 要求:`cc-usage-bar` ≥ **0.4.1**(自适应换行 + 周限额上色 + countdown 预设 + 颜色 ramp 自定义 + tint reverse 样式)。已装旧版的话用 `npm i -g cc-usage-bar@latest` 升级。
45
+
46
+ ### 3.3 读取现有 statusLine.command
47
+
48
+ - **macOS / Linux**:`~/.claude/settings.json`,字段 `statusLine.command`
49
+ - **Windows**:`%USERPROFILE%\.claude\settings.json` 同上
50
+
51
+ 不存在或为空 → 视为「无前缀」。
52
+
53
+ ### 3.4 剥离旧的 cc-usage-fetch / cc-usage-bar-wrap
54
+
55
+ 如果原命令包含 `cc-usage-fetch` 或 `cc-usage-bar-wrap` 字样,**先剥掉那部分**,剩下的才是用户真正的前缀脚本(可能是 `sh ~/.claude/statusline-command.sh` 这种)。常见模式:
56
+
57
+ ```
58
+ sh -c '<前缀脚本>; printf " "; cc-usage-fetch ...'
59
+ ```
60
+ 要把 `<前缀脚本>` 抽出来作为新的 `--prefix=`。
61
+
62
+ ### 3.5 备份 settings.json
63
+
64
+ - macOS/Linux:`cp ~/.claude/settings.json ~/.claude/settings.json.bak.$(date +%s)`
65
+ - Windows:`Copy-Item $env:USERPROFILE\.claude\settings.json "$env:USERPROFILE\.claude\settings.json.bak.$([DateTimeOffset]::Now.ToUnixTimeSeconds())"`
66
+
67
+ ### 3.6 写回 statusLine.command
68
+
69
+ **保留 settings.json 其它字段不动**,只更新这一项;`refreshInterval: 30`。
70
+
71
+ | 情况 | 新 command |
72
+ |---|---|
73
+ | 有前缀脚本 | `cc-usage-bar-wrap --prefix='<前缀>' --format=bar-countdown` |
74
+ | 无前缀(只显示用量条) | `cc-usage-fetch --format=bar-countdown` |
75
+
76
+ `bar-countdown` 是 v0.4.0 起的默认预设(既显示进度条又显示倒计时)。如果用户偏好旧风格,把它换成 `--format=bar-time`、`--format=bar`、`--format=compact` 即可。
77
+
78
+ **关键禁忌**:
79
+
80
+ - ❌ 不要再写 `sh -c '...'` 包裹 `cc-usage-bar-wrap` —— wrap 内部已处理 stdin tee + 子进程 + 自适应换行。
81
+ - ❌ 前缀脚本结尾不能有多余 `printf "\n"`(会让用量条挤到第二行被裁剪,wrap 会自己 trim 但仍最好清掉)。
82
+ - ❌ Windows cmd 里单引号无效,前缀有特殊字符时改用双引号 + 反斜杠转义,或用环境变量 `CC_STATUSLINE_PREFIX` 喂给 wrap。
83
+
84
+ ### 3.7 自检
85
+
86
+ 跑一次假数据:
87
+
88
+ ```sh
89
+ echo '{"workspace":{"current_dir":"."},"model":{"display_name":"Opus"}}' \
90
+ | cc-usage-bar-wrap --prefix='<原前缀>' --layout=single --format=bar-time
91
+ ```
92
+
93
+ 输出应包含 `[`、`█`、`░` 等字符。报 `No Claude Code credentials found` 时让用户先 `claude` 登录一次。
94
+
95
+ ## 4. 自适应换行
96
+
97
+ `cc-usage-bar-wrap` 默认 `--layout=auto`:
98
+
99
+ - 通过 `stty size </dev/tty`(Unix)或 `mode con`(Windows)拿真实终端列数
100
+ - 剥 ANSI 数字符(UTF-8 字符数,不是字节数)
101
+ - 单行能装下 → `prefix bar`;装不下 → `prefix\nbar`
102
+ - 拿不到列数 → 回退多行(最安全)
103
+
104
+ 强制行为:
105
+
106
+ ```sh
107
+ cc-usage-bar-wrap --layout=single # 永远单行
108
+ cc-usage-bar-wrap --layout=multi # 永远换行
109
+ # 或用环境变量
110
+ CC_STATUSLINE_LAYOUT=single
111
+ ```
112
+
113
+ ## 5. 风格预设(`--format`)
114
+
115
+ | 预设 | 渲染 |
116
+ |---|---|
117
+ | `compact`(默认) | `5h 47% Wk 59%` |
118
+ | `numeric` | `47% / 59%` |
119
+ | `time` | `47% until 18:23 / 59% until 5/12 09:00` |
120
+ | `countdown` | `47% in 1h23m / 59% in 2d6h` |
121
+ | `bar` | `[█████░░░░░] 47% / [██████░░░░] 59%` |
122
+ | `bar-time` | `[█████░░░░░] 47% until 18:23 / [██████░░░░] 59% until 5/12 09:00` |
123
+ | `bar-countdown` | `[█████░░░░░] 47% in 1h23m / [██████░░░░] 59% in 2d6h` |
124
+
125
+ 自定义模板(环境变量,覆盖 `--format`):
126
+
127
+ ```sh
128
+ CC_USAGE_TEMPLATE='{label} {percent}% ({countdown} left)'
129
+ # 占位符: {label} {percent} {bar} {expiry} {countdown} {provider} {amount}
130
+ ```
131
+
132
+ ## 6. 颜色 ramp(5h / Wk / 余额各自可定制)
133
+
134
+ 默认(**v0.4.0 起 5h 和 Wk 都默认上色**):
135
+
136
+ | 区间 | 颜色 |
137
+ |---|---|
138
+ | 0–60% | `green` |
139
+ | 60–85% | `yellow` |
140
+ | 85–100% | `red` |
141
+
142
+ 环境变量覆盖:
143
+
144
+ ```sh
145
+ # 5 小时窗口
146
+ CC_USAGE_COLORS_5H='0:green,60:yellow,85:red'
147
+ # 7 天窗口(可以更激进,比如临界变粗红)
148
+ CC_USAGE_COLORS_WK='0:green,60:yellow,85:boldRed'
149
+ # 余额型 provider
150
+ CC_USAGE_COLORS_BALANCE='0:cyan,60:yellow,90:red'
151
+ ```
152
+
153
+ 格式:`<min>:<color>` 用逗号分隔,`min` 可以是小数。
154
+
155
+ **颜色 token 三种形态**:
156
+
157
+ | 类型 | 例 | 说明 |
158
+ |---|---|---|
159
+ | 命名色 | `red` `boldRed` `dim` 等 | 16 色调色板 + bold 变体 |
160
+ | Hex | `#ff3333` `#fa0` | 24-bit truecolor,需要终端支持 |
161
+ | `none` | `none` | 这一区间不上色 |
162
+
163
+ 完整命名色:`green` `yellow` `red` `blue` `magenta` `cyan` `white` `gray` `dim`、
164
+ `boldGreen` `boldYellow` `boldRed` `boldBlue` `boldMagenta` `boldCyan` `boldWhite`。
165
+
166
+ 例:`CC_USAGE_COLORS_WK='0:#888888,50:#ffaa00,80:#ff3333,95:boldRed'`
167
+
168
+ ## 7. 进度条样式(`--bar-spec`,**5h 和 Wk 共用**)
169
+
170
+ ```sh
171
+ # 自定义填充/空槽字符(cells)
172
+ --bar-spec='{"mode":"cells","filled":"▰","empty":"▱","width":12}'
173
+
174
+ # 单色渐变(tint)—— 完成部分上色,剩余部分变暗
175
+ --bar-spec='{"mode":"tint","text":"████████","emptyStyle":"dim"}'
176
+
177
+ # 反色填充(reverse)—— 完成部分用反色底块增强对比
178
+ --bar-spec='{"mode":"tint","text":"Ciallo~(∠・ω< )⌒★","style":"reverse"}'
179
+
180
+ # 动画帧(frames)—— 按百分比从帧序列里挑一帧
181
+ --bar-spec='{"mode":"frames","frames":["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]}'
182
+ ```
183
+
184
+ 环境变量 `CC_USAGE_BAR_SPEC` 同上。Windows cmd 里单引号无效,改 `"{\"mode\":...}"`。
185
+
186
+ ## 8. 凭证查找顺序
187
+
188
+ | 平台 | 1 | 2 | 3 |
189
+ |---|---|---|---|
190
+ | macOS | 钥匙串 `Claude Code-credentials` | `~/.claude/.credentials.json` | — |
191
+ | Windows | Credential Manager `Claude Code-credentials` | `%USERPROFILE%\.claude\.credentials.json` | — |
192
+ | Linux | `~/.claude/.credentials.json` | — | — |
193
+
194
+ 非 Anthropic provider 用 `ANTHROPIC_AUTH_TOKEN` 环境变量。
195
+
196
+ ## 9. 缓存与请求节流
197
+
198
+ - 成功结果缓存 30 秒(`/tmp/cc-oauth-usage.json` 或 OS tmpdir)
199
+ - 鉴权失败缓存 60 秒
200
+ - 限流(429)/ 5xx 缓存 60 秒(v0.3.0 起)—— 防止重试风暴
201
+ - Claude Code 注入 stdin `rate_limits` 时直接用,**0 次** API 调用
202
+
203
+ 诊断:`CC_USAGE_LOG=/tmp/ccu.log` 让 `cc-usage-fetch` 每次写一行 JSON(`source` / `cacheStatus` / `error`)。
204
+
205
+ ## 10. 排障
206
+
207
+ | 现象 | 处理 |
208
+ |---|---|
209
+ | `No Claude Code credentials found` | `claude` 登录一次 |
210
+ | 用量条没颜色 | v0.3.x 之前 Wk 默认无色,升级到 0.4.0+ |
211
+ | 窄屏被截 | 确认 statusLine.command 用的是 `cc-usage-bar-wrap`,不是直接 `cc-usage-fetch` |
212
+ | 单行/换行不切换 | 确认 `--layout=auto`(默认),并确认环境里有 `/dev/tty`(Unix)或可用 console(Windows) |
213
+ | Windows cmd 看不到颜色 | 换 Windows Terminal / PowerShell 7,或加 `--plain` |
214
+ | 余额 provider 显示「No credits」 | 检查 `ANTHROPIC_AUTH_TOKEN` 是否对应该 provider |
215
+ | settings.json 写坏 | `cp ~/.claude/settings.json.bak.<timestamp> ~/.claude/settings.json` |
216
+
217
+ ## 11. 完成后该说什么
218
+
219
+ 告诉用户:
220
+
221
+ > ✅ 装好了。下次 statusline 刷新(≤ 30 秒)就能看到,不需要重启 Claude Code。试着拉宽 / 拉窄窗口,进度条会单行 ↔ 换行自动切换。
222
+
223
+ ## 12. 用户偏好默认值(用户没说就这么用)
224
+
225
+ - `--format=bar-countdown`(v0.4.0 起的默认)
226
+ - `--bar-width=10`
227
+ - `--layout=auto`
228
+ - 颜色保留,按默认 ramp(绿/黄/红)
229
+ - 不加自定义 `--bar-spec`
230
+
231
+ 如用户说「极简」「窄屏」「我屏幕只有 80 列」→ 改 `--format=compact`。
232
+ 如用户说「我想看还剩多久」→ 改 `--format=countdown` 或 `--format=bar-countdown`。
233
+ 如用户说「Wk 颜色不一样」「Wk 紧迫的时候要更醒目」→ 设 `CC_USAGE_COLORS_WK='0:gray,80:boldRed'` 之类。
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # cc-usage-bar
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/cc-usage-bar.svg)](https://www.npmjs.com/package/cc-usage-bar)
4
+ [![npm downloads](https://img.shields.io/npm/dm/cc-usage-bar.svg)](https://www.npmjs.com/package/cc-usage-bar)
5
+ [![license](https://img.shields.io/npm/l/cc-usage-bar.svg)](LICENSE)
6
+ [![node](https://img.shields.io/node/v/cc-usage-bar.svg)](package.json)
7
+ [![GitHub](https://img.shields.io/badge/GitHub-TuYv%2Fcc--usage--bar-181717?logo=github)](https://github.com/TuYv/cc-usage-bar)
8
+
3
9
  [中文说明](README.zh-CN.md)
4
10
 
5
11
  Show your Claude Code subscription usage in the statusline. Works with the official Anthropic subscription and 8 alternative providers (Kimi, GLM/Zhipu, MiniMax, DeepSeek, StepFun, SiliconFlow, OpenRouter, Novita).
@@ -43,7 +49,7 @@ A Claude Code statusline is just a shell command — its stdout is rendered to t
43
49
 
44
50
  1. **stdin `rate_limits`** — Claude Code now passes a `rate_limits` object to statusline stdin when you're a Claude.ai subscriber. Zero auth, zero network.
45
51
  2. **Local cache** — `/tmp/cc-oauth-usage.json`, 30 s TTL on success, 60 s on auth failure (so a stale token doesn't hammer the API).
46
- 3. **Provider HTTP query** — picked from `ANTHROPIC_BASE_URL`. For Anthropic, the OAuth token is read from your macOS keychain (or `~/.claude/.credentials.json`). For the rest, `ANTHROPIC_AUTH_TOKEN` from the env.
52
+ 3. **Provider HTTP query** — picked from `ANTHROPIC_BASE_URL`. For Anthropic, the OAuth token is read by platform: macOS keychain Windows Credential Manager → `~/.claude/.credentials.json`. For the rest, `ANTHROPIC_AUTH_TOKEN` from the env.
47
53
 
48
54
  ## Supported providers
49
55
 
@@ -111,6 +117,12 @@ cc-usage-bar install --format=bar-time \
111
117
  {"mode":"tint","text":"Ciallo~(∠・ω< )⌒★"}
112
118
  ```
113
119
 
120
+ For stronger contrast, add `style:"reverse"` so the completed prefix is rendered as a reversed-color block:
121
+
122
+ ```json
123
+ {"mode":"tint","text":"Ciallo~(∠・ω< )⌒★","style":"reverse"}
124
+ ```
125
+
114
126
  `cells` replaces the default block characters:
115
127
 
116
128
  ```json
@@ -172,7 +184,7 @@ base url: <not set, defaults to anthropic>
172
184
 
173
185
  ## Privacy & API stability
174
186
 
175
- This tool calls **two undocumented Anthropic endpoints**: `/api/oauth/usage` (subscription quota) and reads OAuth tokens from the local macOS keychain. The `cc-switch` project (Tauri/Rust) has used the same approach in production for months. **Both endpoints may change without notice** — open an issue if you see a regression.
187
+ This tool calls **one undocumented Anthropic endpoint**: `/api/oauth/usage` (subscription quota), and reads OAuth tokens from local OS-native credential stores (macOS keychain, Windows Credential Manager, or the JSON file). The `cc-switch` project (Tauri/Rust) has used the same approach in production for months. **The endpoint may change without notice** — open an issue if you see a regression.
176
188
 
177
189
  Tokens are read **only on your machine**. Nothing is uploaded anywhere. The tool makes one HTTPS request per refresh interval to the provider you've configured (or zero, when `rate_limits` is in stdin).
178
190
 
@@ -180,7 +192,7 @@ For non-Anthropic providers, the same is true: the API key from `ANTHROPIC_AUTH_
180
192
 
181
193
  ## Why no `jq` / `curl` dependency?
182
194
 
183
- Other statusline tools (e.g. `ccusage`) shell out to `jq` and `curl`. This one is pure Node.js (built-in `fetch` from Node 18+, `child_process` for the macOS keychain only). One `npm install -g` and you're done — no system tooling required.
195
+ Other statusline tools (e.g. `ccusage`) shell out to `jq` and `curl`. This one is pure Node.js (built-in `fetch` from Node 18+, `child_process` only for the macOS keychain or Windows Credential Manager call). One `npm install -g` and you're done — no system tooling required, works on macOS / Linux / Windows.
184
196
 
185
197
  ## Acknowledgements
186
198
 
package/README.zh-CN.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # 中文说明
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/cc-usage-bar.svg)](https://www.npmjs.com/package/cc-usage-bar)
4
+ [![npm downloads](https://img.shields.io/npm/dm/cc-usage-bar.svg)](https://www.npmjs.com/package/cc-usage-bar)
5
+ [![license](https://img.shields.io/npm/l/cc-usage-bar.svg)](LICENSE)
6
+ [![node](https://img.shields.io/node/v/cc-usage-bar.svg)](package.json)
7
+ [![GitHub](https://img.shields.io/badge/GitHub-TuYv%2Fcc--usage--bar-181717?logo=github)](https://github.com/TuYv/cc-usage-bar)
8
+
3
9
  [English](README.md)
4
10
 
5
11
  `cc-usage-bar` 可以把 Claude Code 的订阅用量显示在底部 statusline。它支持官方 Anthropic 订阅,也支持 Kimi、GLM/Zhipu、MiniMax、DeepSeek、StepFun、SiliconFlow、OpenRouter、Novita 等替代 provider。
@@ -45,7 +51,7 @@ Claude Code 的 statusline 本质上是一条 shell 命令,命令的 stdout
45
51
 
46
52
  1. **stdin `rate_limits`**:Claude Code 会把订阅用户的 `rate_limits` 传给 statusline,无需鉴权、无需网络。
47
53
  2. **本地缓存**:`/tmp/cc-oauth-usage.json`,成功缓存 30 秒,鉴权失败缓存 60 秒。
48
- 3. **Provider HTTP 查询**:根据 `ANTHROPIC_BASE_URL` 自动选择 provider。Anthropic 会读取 macOS keychain `~/.claude/.credentials.json`;其他 provider 使用环境变量 `ANTHROPIC_AUTH_TOKEN`。
54
+ 3. **Provider HTTP 查询**:根据 `ANTHROPIC_BASE_URL` 自动选择 provider。Anthropic 按平台读取凭证:macOS 钥匙串 Windows Credential Manager → `~/.claude/.credentials.json`;其他 provider 使用环境变量 `ANTHROPIC_AUTH_TOKEN`。
49
55
 
50
56
  ## 支持的 Provider
51
57
 
@@ -116,6 +122,12 @@ cc-usage-bar install --format=bar-time \
116
122
  {"mode":"tint","text":"Ciallo~(∠・ω< )⌒★"}
117
123
  ```
118
124
 
125
+ 如果想让完成部分更像“填充底块”,可以加 `style:"reverse"`,完成部分会用反色块增强对比:
126
+
127
+ ```json
128
+ {"mode":"tint","text":"Ciallo~(∠・ω< )⌒★","style":"reverse"}
129
+ ```
130
+
119
131
  #### cells:替换默认块字符
120
132
 
121
133
  ```json
@@ -179,7 +191,7 @@ base url: <not set, defaults to anthropic>
179
191
 
180
192
  ## 隐私与 API 稳定性
181
193
 
182
- Anthropic 订阅用量依赖未公开接口 `/api/oauth/usage`,并会从本机 macOS keychain `~/.claude/.credentials.json` 读取 Claude Code OAuth token。这些接口可能变化。
194
+ Anthropic 订阅用量依赖未公开接口 `/api/oauth/usage`,并按平台读取本机 OAuth token:macOS 钥匙串、Windows Credential Manager、或 `~/.claude/.credentials.json`。这些接口可能变化。
183
195
 
184
196
  Token 只在你的机器上读取,不会上传到本项目的任何服务。工具只会请求你配置的 provider。使用 stdin `rate_limits` 时不需要网络。
185
197
 
@@ -187,7 +199,7 @@ Token 只在你的机器上读取,不会上传到本项目的任何服务。
187
199
 
188
200
  ## 为什么不依赖 jq / curl
189
201
 
190
- 这个工具是纯 Node.js 实现,Node 18+ 自带 `fetch`,只在 macOS 读取 keychain 时使用 `child_process`。安装后直接可用,不需要额外安装 `jq` 或 `curl`。
202
+ 这个工具是纯 Node.js 实现,Node 18+ 自带 `fetch`,只在读取 macOS 钥匙串或 Windows Credential Manager 时使用 `child_process`。安装后直接可用,不需要额外安装 `jq` 或 `curl`,跨 macOS / Linux / Windows。
191
203
 
192
204
  ## 致谢
193
205
 
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/src/wrap.js')
3
+ .main()
4
+ .catch(() => {
5
+ // silent — never pollute the statusline
6
+ process.exit(0);
7
+ });
package/dist/src/cli.js CHANGED
@@ -49,25 +49,6 @@ function readPkgVersion() {
49
49
  return '0.0.0';
50
50
  }
51
51
  }
52
- function formatCountdown(iso) {
53
- if (!iso)
54
- return 'unknown';
55
- const target = new Date(iso).getTime();
56
- if (Number.isNaN(target))
57
- return 'unknown';
58
- const diffMs = target - Date.now();
59
- if (diffMs <= 0)
60
- return 'now';
61
- const totalMin = Math.floor(diffMs / 60_000);
62
- const days = Math.floor(totalMin / (60 * 24));
63
- const hours = Math.floor((totalMin % (60 * 24)) / 60);
64
- const mins = totalMin % 60;
65
- if (days > 0)
66
- return `${days}d ${hours}h ${mins}m`;
67
- if (hours > 0)
68
- return `${hours}h ${mins}m`;
69
- return `${mins}m`;
70
- }
71
52
  function loadSettingsOrExit() {
72
53
  try {
73
54
  return (0, settings_1.readSettings)();
@@ -169,9 +150,9 @@ async function runStatus() {
169
150
  if (result.data.planName)
170
151
  console.log(`plan: ${result.data.planName}`);
171
152
  if (fh)
172
- console.log(`5-hour: ${Math.round(fh.utilization)}% (resets in ${formatCountdown(fh.resets_at)})`);
153
+ console.log(`5-hour: ${Math.round(fh.utilization)}% (resets in ${(0, format_1.formatCountdown)(fh.resets_at)})`);
173
154
  if (wk)
174
- console.log(`7-day: ${Math.round(wk.utilization)}% (resets in ${formatCountdown(wk.resets_at)})`);
155
+ console.log(`7-day: ${Math.round(wk.utilization)}% (resets in ${(0, format_1.formatCountdown)(wk.resets_at)})`);
175
156
  }
176
157
  else {
177
158
  const d = result.data;
@@ -193,12 +174,12 @@ program
193
174
  .command('install')
194
175
  .description('Install statusline integration into ~/.claude/settings.json')
195
176
  .option('-f, --force', 'overwrite if already installed')
196
- .option('--format <preset>', `render preset (${format_1.FORMAT_PRESETS.join(' | ')})`, 'compact')
177
+ .option('--format <preset>', `render preset (${format_1.FORMAT_PRESETS.join(' | ')})`, 'bar-countdown')
197
178
  .option('--bar-width <n>', 'bar width when format includes a bar (1-50)', '10')
198
179
  .option('--bar-spec <json>', 'custom bar JSON spec (cells, tint, or frames)')
199
180
  .action(async (opts) => {
200
181
  const installOpts = {};
201
- if (opts.format && opts.format !== 'compact') {
182
+ if (opts.format && opts.format !== 'bar-countdown') {
202
183
  if (!(0, format_1.isValidPreset)(opts.format)) {
203
184
  console.error(`error: --format must be one of ${format_1.FORMAT_PRESETS.join(', ')}`);
204
185
  process.exit(1);
@@ -235,6 +216,25 @@ program
235
216
  .action(async () => {
236
217
  await runStatus();
237
218
  });
219
+ program
220
+ .command('agents')
221
+ .description('Print the AI-readable install guide (for piping into Claude Code, Cursor, etc.)')
222
+ .action(() => {
223
+ const candidates = [
224
+ path.join(__dirname, '..', '..', 'AGENTS.md'),
225
+ path.join(__dirname, '..', 'AGENTS.md'),
226
+ ];
227
+ for (const p of candidates) {
228
+ try {
229
+ process.stdout.write(fs.readFileSync(p, 'utf8'));
230
+ return;
231
+ }
232
+ catch {
233
+ }
234
+ }
235
+ console.error('AGENTS.md not found in package.');
236
+ process.exit(1);
237
+ });
238
238
  program.parseAsync(process.argv).catch((e) => {
239
239
  console.error(e instanceof Error ? e.message : String(e));
240
240
  process.exit(1);
package/dist/src/fetch.js CHANGED
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.readStdinJson = readStdinJson;
37
37
  exports.extractFromStdin = extractFromStdin;
38
38
  exports.run = run;
39
+ exports.parseArg = parseArg;
39
40
  exports.main = main;
40
41
  const fs = __importStar(require("node:fs"));
41
42
  const os = __importStar(require("node:os"));
@@ -46,6 +47,7 @@ const format_1 = require("./format");
46
47
  const CACHE_PATH = path.join(os.tmpdir(), 'cc-oauth-usage.json');
47
48
  const SUCCESS_TTL_MS = 30_000;
48
49
  const AUTH_FAIL_TTL_MS = 60_000;
50
+ const TRANSIENT_FAIL_TTL_MS = 60_000;
49
51
  const STDIN_TIMEOUT_MS = 500;
50
52
  async function readStdinJson(timeoutMs = STDIN_TIMEOUT_MS) {
51
53
  if (process.stdin.isTTY)
@@ -147,15 +149,19 @@ function writeCache(entry) {
147
149
  }
148
150
  function isCacheFresh(entry) {
149
151
  const age = Date.now() - entry.fetched_at;
152
+ if (age < 0)
153
+ return false;
150
154
  if (entry.status === 'ok')
151
- return age >= 0 && age < SUCCESS_TTL_MS;
155
+ return age < SUCCESS_TTL_MS;
152
156
  if (entry.status === 'auth_failed')
153
- return age >= 0 && age < AUTH_FAIL_TTL_MS;
157
+ return age < AUTH_FAIL_TTL_MS;
158
+ if (entry.status === 'rate_limited')
159
+ return age < TRANSIENT_FAIL_TTL_MS;
154
160
  return false;
155
161
  }
156
162
  async function run(opts = {}) {
157
163
  if (!opts.skipStdin) {
158
- const stdinJson = await readStdinJson();
164
+ const stdinJson = opts.stdinJson !== undefined ? opts.stdinJson : await readStdinJson();
159
165
  const fromStdin = stdinAsSubscription(extractFromStdin(stdinJson));
160
166
  if (fromStdin) {
161
167
  return {
@@ -181,12 +187,18 @@ async function run(opts = {}) {
181
187
  }
182
188
  const cache = opts.forceFresh ? null : readCache();
183
189
  if (cache && cache.adapter_id === adapter.id && isCacheFresh(cache)) {
190
+ let cacheStatusReport = 'hit';
191
+ if (cache.status === 'auth_failed')
192
+ cacheStatusReport = 'auth_failed_cached';
193
+ else if (cache.status === 'rate_limited')
194
+ cacheStatusReport = 'rate_limited_cached';
184
195
  return {
185
196
  data: cache.data,
186
197
  source: 'cache',
187
198
  adapterId: adapter.id,
188
- cacheStatus: cache.status === 'auth_failed' ? 'auth_failed_cached' : 'hit',
199
+ cacheStatus: cacheStatusReport,
189
200
  authFailed: cache.status === 'auth_failed',
201
+ error: cache.error,
190
202
  };
191
203
  }
192
204
  const cacheStatusOnMiss = cache && cache.adapter_id !== adapter.id ? 'adapter_changed' : cache ? 'expired' : 'miss';
@@ -202,7 +214,13 @@ async function run(opts = {}) {
202
214
  };
203
215
  }
204
216
  if (result.authFailed) {
205
- writeCache({ fetched_at: Date.now(), status: 'auth_failed', adapter_id: adapter.id, data: null });
217
+ writeCache({
218
+ fetched_at: Date.now(),
219
+ status: 'auth_failed',
220
+ adapter_id: adapter.id,
221
+ data: null,
222
+ error: result.error,
223
+ });
206
224
  return {
207
225
  data: null,
208
226
  source: 'api',
@@ -212,6 +230,17 @@ async function run(opts = {}) {
212
230
  error: result.error,
213
231
  };
214
232
  }
233
+ const httpStatus = result.status ?? 0;
234
+ const isTransientHttp = httpStatus === 429 || (httpStatus >= 500 && httpStatus < 600);
235
+ if (isTransientHttp) {
236
+ writeCache({
237
+ fetched_at: Date.now(),
238
+ status: 'rate_limited',
239
+ adapter_id: adapter.id,
240
+ data: null,
241
+ error: result.error,
242
+ });
243
+ }
215
244
  return {
216
245
  data: null,
217
246
  source: 'api',
@@ -221,6 +250,17 @@ async function run(opts = {}) {
221
250
  error: result.error,
222
251
  };
223
252
  }
253
+ function logEvent(event) {
254
+ const target = process.env.CC_USAGE_LOG;
255
+ if (!target)
256
+ return;
257
+ try {
258
+ const line = JSON.stringify({ ts: new Date().toISOString(), pid: process.pid, ...event }) + '\n';
259
+ fs.appendFileSync(target, line);
260
+ }
261
+ catch {
262
+ }
263
+ }
224
264
  function parseArg(args, name) {
225
265
  const eq = args.find((a) => a.startsWith(`--${name}=`));
226
266
  if (eq)
@@ -250,14 +290,27 @@ Custom template (env, overrides --format):
250
290
  CC_USAGE_BAR_SPEC Same JSON shape as --bar-spec
251
291
 
252
292
  Presets render like:
253
- compact 5h 47% Wk 59%
254
- numeric 47% / 59%
255
- time 47% until 18:23 / 59% until 5/12 09:00
256
- bar [█████░░░░░] 47% / [██████░░░░] 59%
257
- bar-time [█████░░░░░] 47% until 18:23 / [██████░░░░] 59% until 5/12 09:00
293
+ compact 5h 47% Wk 59%
294
+ numeric 47% / 59%
295
+ time 47% until 18:23 / 59% until 5/12 09:00
296
+ countdown 47% in 1h23m / 59% in 2d6h
297
+ bar [█████░░░░░] 47% / [██████░░░░] 59%
298
+ bar-time [█████░░░░░] 47% until 18:23 / [██████░░░░] 59% until 5/12 09:00
299
+ bar-countdown [█████░░░░░] 47% in 1h23m / [██████░░░░] 59% in 2d6h
300
+
301
+ Per-tier color ramps (override defaults — comma-separated "<min>:<color>"):
302
+ CC_USAGE_COLORS_5H e.g. '0:green,60:yellow,85:#ff3333'
303
+ CC_USAGE_COLORS_WK e.g. '0:#888,60:#ffaa00,85:boldRed'
304
+ CC_USAGE_COLORS_BALANCE used by balance providers (DeepSeek/OpenRouter/...)
305
+ Color tokens:
306
+ - named: green/yellow/red/blue/magenta/cyan/white/gray/dim,
307
+ boldGreen/boldYellow/boldRed/boldBlue/boldMagenta/boldCyan/boldWhite
308
+ - hex: '#RRGGBB' or '#RGB' (24-bit truecolor; needs a truecolor terminal)
309
+ - 'none': skip color in that band
258
310
 
259
311
  Other:
260
312
  NO_COLOR=1 Force-disable colors
313
+ CC_USAGE_LOG=<path> Append one JSON line per invocation (source / cacheStatus / error) for diagnosing call frequency
261
314
  --help Show this message
262
315
  `;
263
316
  async function main() {
@@ -287,7 +340,24 @@ async function main() {
287
340
  const barSpec = (0, format_1.parseBarSpec)(barSpecArg ?? barSpecEnv);
288
341
  if (barSpec)
289
342
  fmt.barSpec = barSpec;
343
+ const RAMP_ENV = [
344
+ ['colorRamp5h', 'CC_USAGE_COLORS_5H'],
345
+ ['colorRampWk', 'CC_USAGE_COLORS_WK'],
346
+ ['colorRampBalance', 'CC_USAGE_COLORS_BALANCE'],
347
+ ];
348
+ for (const [key, envName] of RAMP_ENV) {
349
+ const ramp = (0, format_1.parseColorRamp)(process.env[envName]);
350
+ if (ramp)
351
+ fmt[key] = ramp;
352
+ }
290
353
  const result = await run({ skipStdin });
354
+ logEvent({
355
+ source: result.source,
356
+ cacheStatus: result.cacheStatus,
357
+ adapterId: result.adapterId,
358
+ authFailed: result.authFailed,
359
+ error: result.error ?? null,
360
+ });
291
361
  if (wantJson) {
292
362
  process.stdout.write(JSON.stringify({
293
363
  data: result.data,