cc-usage-bar 0.1.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/LICENSE +21 -0
- package/README.md +191 -0
- package/README.zh-CN.md +198 -0
- package/bin/cc-usage-fetch.js +7 -0
- package/bin/cc-usage-statusline.js +2 -0
- package/dist/src/cli.js +241 -0
- package/dist/src/fetch.js +307 -0
- package/dist/src/format.js +229 -0
- package/dist/src/providers/anthropic.js +55 -0
- package/dist/src/providers/deepseek.js +55 -0
- package/dist/src/providers/glm.js +59 -0
- package/dist/src/providers/http.js +26 -0
- package/dist/src/providers/kimi.js +57 -0
- package/dist/src/providers/minimax.js +66 -0
- package/dist/src/providers/novita.js +43 -0
- package/dist/src/providers/openrouter.js +47 -0
- package/dist/src/providers/registry.js +40 -0
- package/dist/src/providers/siliconflow.js +55 -0
- package/dist/src/providers/stepfun.js +52 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/render.js +29 -0
- package/dist/src/settings.js +215 -0
- package/dist/src/token.js +85 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cc-usage-statusline contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# cc-usage-bar
|
|
2
|
+
|
|
3
|
+
[中文说明](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
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).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
5h 47% Wk 59%
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or, with `--format=bar-time`:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
[█████░░░░░] 47% until 18:00 / [██████░░░░] 59% until 5/9 09:00
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g cc-usage-bar
|
|
21
|
+
cc-usage-bar install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Restart Claude Code. That's it.
|
|
25
|
+
|
|
26
|
+
To uninstall:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cc-usage-bar uninstall
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The original `~/.claude/settings.json` is restored from a timestamped backup.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
A Claude Code statusline is just a shell command — its stdout is rendered to the bottom of the TUI. This package ships these binaries:
|
|
37
|
+
|
|
38
|
+
- `cc-usage-fetch` — emits one statusline string. Called by Claude Code 30 s by default.
|
|
39
|
+
- `cc-usage-bar` — installer. Wires `cc-usage-fetch` into `~/.claude/settings.json`'s `statusLine.command`. If you already have a statusline command, it's wrapped, not replaced.
|
|
40
|
+
- `cc-usage-statusline` — compatibility alias for `cc-usage-bar`.
|
|
41
|
+
|
|
42
|
+
`cc-usage-fetch` resolves usage data via three fallbacks, in order:
|
|
43
|
+
|
|
44
|
+
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
|
+
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.
|
|
47
|
+
|
|
48
|
+
## Supported providers
|
|
49
|
+
|
|
50
|
+
Provider detection runs against `ANTHROPIC_BASE_URL` (set by Claude Code from `settings.json` `env`, or by tools like cc-switch).
|
|
51
|
+
|
|
52
|
+
| Type | Provider | Detected URL substring |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Subscription (5h + 7d sliding window) | Anthropic | `anthropic.com` (or unset) |
|
|
55
|
+
| Subscription | Kimi | `api.kimi.com` |
|
|
56
|
+
| Subscription | GLM (Zhipu) | `z.ai`, `bigmodel.cn` |
|
|
57
|
+
| Subscription | MiniMax | `minimaxi.com`, `minimax.io` |
|
|
58
|
+
| Balance | DeepSeek | `api.deepseek.com` |
|
|
59
|
+
| Balance | StepFun | `api.stepfun.com`, `api.stepfun.ai` |
|
|
60
|
+
| Balance | SiliconFlow | `api.siliconflow.cn`, `api.siliconflow.com` |
|
|
61
|
+
| Balance | OpenRouter | `openrouter.ai` |
|
|
62
|
+
| Balance | Novita | `api.novita.ai` |
|
|
63
|
+
|
|
64
|
+
Subscription mode renders `5h X% Wk Y%`. Balance mode renders `¥34.20` or `$5.88/$10.00`. Unknown providers render nothing (no error in the statusline).
|
|
65
|
+
|
|
66
|
+
## Render presets
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cc-usage-bar install --format <preset> --bar-width <n>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Preset | Output |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `compact` (default) | `5h 47% Wk 59%` |
|
|
75
|
+
| `numeric` | `47% / 59%` |
|
|
76
|
+
| `time` | `47% until 18:00 / 59% until 5/9 09:00` |
|
|
77
|
+
| `bar` | `[█████░░░░░] 47% / [██████░░░░] 59%` |
|
|
78
|
+
| `bar-time` | `[█████░░░░░] 47% until 18:00 / [██████░░░░] 59% until 5/9 09:00` |
|
|
79
|
+
|
|
80
|
+
**Time format is local-timezone, smart-truncated**: same day → `HH:MM`, within a week → `M/D HH:MM`, further → `YYYY-MM-DD`.
|
|
81
|
+
|
|
82
|
+
**Color thresholds**: 5h window — green <60%, yellow <85%, red ≥85%. Weekly — bold red ≥80%. Set `NO_COLOR=1` to disable.
|
|
83
|
+
|
|
84
|
+
### Custom template
|
|
85
|
+
|
|
86
|
+
For full control, set `CC_USAGE_TEMPLATE` in `~/.claude/settings.json` `env`. It overrides `--format`.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"env": {
|
|
91
|
+
"CC_USAGE_TEMPLATE": "{label}={percent}% ({expiry})"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Placeholders: `{label}` (5h / Wk) · `{percent}` · `{bar}` · `{expiry}` · `{provider}` · `{amount}` (balance only).
|
|
97
|
+
|
|
98
|
+
### Custom progress bar
|
|
99
|
+
|
|
100
|
+
For AI-assisted personalization, pass one compact JSON object as `--bar-spec` or `CC_USAGE_BAR_SPEC`.
|
|
101
|
+
The tool does not call AI or parse images itself; it only renders the JSON spec. Ask your AI assistant to convert an image, phrase, or theme into one of these shapes:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cc-usage-bar install --format=bar-time \
|
|
105
|
+
--bar-spec='{"mode":"tint","text":"Ciallo~(∠・ω< )⌒★"}'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`tint` keeps the whole text visible and colors the completed prefix while dimming the rest:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{"mode":"tint","text":"Ciallo~(∠・ω< )⌒★"}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`cells` replaces the default block characters:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{"mode":"cells","filled":"●","empty":"○","width":10}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`frames` selects one text frame based on usage percentage:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{"mode":"frames","frames":["(・_・)","(・ω・)","(∠・ω< )","Ciallo~(∠・ω< )⌒★"]}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Suggested prompt for your AI assistant:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
Convert this image or phrase into a cc-usage-bar bar spec.
|
|
130
|
+
Return only one JSON object using mode "tint", "cells", or "frames".
|
|
131
|
+
Prefer "tint" for phrases, faces, logos, and decorative text.
|
|
132
|
+
Keep it single-line and terminal-friendly.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Manually editing the statusline
|
|
136
|
+
|
|
137
|
+
Install just writes a command into `settings.json`. Edit it freely:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
"statusLine": {
|
|
141
|
+
"type": "command",
|
|
142
|
+
"command": "cc-usage-fetch --format=bar-time --bar-width=15",
|
|
143
|
+
"refreshInterval": 30
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Diagnostics
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
cc-usage-bar status
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Shows current provider, source (stdin / cache / api), 5h + 7d countdowns or balance breakdown. Bypasses the cache for accurate readings.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
provider: anthropic
|
|
157
|
+
source: api
|
|
158
|
+
cache status: miss
|
|
159
|
+
base url: <not set, defaults to anthropic>
|
|
160
|
+
5-hour: 47% (resets in 3h 19m)
|
|
161
|
+
7-day: 59% (resets in 9h 39m)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Troubleshooting
|
|
165
|
+
|
|
166
|
+
| Symptom | Cause | Fix |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| Statusline blank | Free tier (no `rate_limits` in stdin) and no Anthropic token, or unknown provider | Run `cc-usage-bar status`; check `ANTHROPIC_BASE_URL` |
|
|
169
|
+
| `unauthorized (401)` in `status` | OAuth token expired or wrong `ANTHROPIC_AUTH_TOKEN` | Re-login: `claude` (Anthropic), or update token in cc-switch / settings.json |
|
|
170
|
+
| Statusline shows old data | 30 s success cache | Wait, or `rm /tmp/cc-oauth-usage.json` |
|
|
171
|
+
| Bar chars look like `??` | Terminal can't render Unicode block elements (U+2588, U+2591) | Use `--format=numeric` or set a Unicode-capable font |
|
|
172
|
+
|
|
173
|
+
## Privacy & API stability
|
|
174
|
+
|
|
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.
|
|
176
|
+
|
|
177
|
+
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
|
+
|
|
179
|
+
For non-Anthropic providers, the same is true: the API key from `ANTHROPIC_AUTH_TOKEN` is sent only to the provider's published balance/quota endpoint.
|
|
180
|
+
|
|
181
|
+
## Why no `jq` / `curl` dependency?
|
|
182
|
+
|
|
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.
|
|
184
|
+
|
|
185
|
+
## Acknowledgements
|
|
186
|
+
|
|
187
|
+
The provider adapter logic — endpoints, response parsing, the OAuth token discovery dance — is a Node port of [cc-switch](https://github.com/farion1231/cc-switch)'s Rust implementation (`src-tauri/src/services/{subscription,coding_plan,balance}.rs`). Thanks to that project for reverse-engineering and maintaining the Chinese-provider integrations.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# 中文说明
|
|
2
|
+
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
`cc-usage-bar` 可以把 Claude Code 的订阅用量显示在底部 statusline。它支持官方 Anthropic 订阅,也支持 Kimi、GLM/Zhipu、MiniMax、DeepSeek、StepFun、SiliconFlow、OpenRouter、Novita 等替代 provider。
|
|
6
|
+
|
|
7
|
+
默认效果:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
5h 47% Wk 59%
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
进度条 + 重置时间:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
[█████░░░░░] 47% until 18:00 / [██████░░░░] 59% until 5/9 09:00
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g cc-usage-bar
|
|
23
|
+
cc-usage-bar install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
然后重启 Claude Code。
|
|
27
|
+
|
|
28
|
+
卸载:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cc-usage-bar uninstall
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
安装时会备份原来的 `~/.claude/settings.json`。如果你已经有自己的 statusline 命令,本工具会包装它,不会直接覆盖。
|
|
35
|
+
|
|
36
|
+
## 工作原理
|
|
37
|
+
|
|
38
|
+
Claude Code 的 statusline 本质上是一条 shell 命令,命令的 stdout 会显示在 TUI 底部。本项目提供这些命令:
|
|
39
|
+
|
|
40
|
+
- `cc-usage-fetch`:输出一行 statusline 文本,默认由 Claude Code 每 30 秒调用一次。
|
|
41
|
+
- `cc-usage-bar`:安装器,把 `cc-usage-fetch` 写入 `~/.claude/settings.json` 的 `statusLine.command`。
|
|
42
|
+
- `cc-usage-statusline`:`cc-usage-bar` 的兼容别名。
|
|
43
|
+
|
|
44
|
+
`cc-usage-fetch` 会按下面顺序获取数据:
|
|
45
|
+
|
|
46
|
+
1. **stdin `rate_limits`**:Claude Code 会把订阅用户的 `rate_limits` 传给 statusline,无需鉴权、无需网络。
|
|
47
|
+
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`。
|
|
49
|
+
|
|
50
|
+
## 支持的 Provider
|
|
51
|
+
|
|
52
|
+
Provider 通过 `ANTHROPIC_BASE_URL` 判断。
|
|
53
|
+
|
|
54
|
+
| 类型 | Provider | URL 关键词 |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| 订阅用量 | Anthropic | `anthropic.com` 或不设置 |
|
|
57
|
+
| 订阅用量 | Kimi | `api.kimi.com` |
|
|
58
|
+
| 订阅用量 | GLM / 智谱 | `z.ai`, `bigmodel.cn` |
|
|
59
|
+
| 订阅用量 | MiniMax | `minimaxi.com`, `minimax.io` |
|
|
60
|
+
| 余额 | DeepSeek | `api.deepseek.com` |
|
|
61
|
+
| 余额 | StepFun | `api.stepfun.com`, `api.stepfun.ai` |
|
|
62
|
+
| 余额 | SiliconFlow | `api.siliconflow.cn`, `api.siliconflow.com` |
|
|
63
|
+
| 余额 | OpenRouter | `openrouter.ai` |
|
|
64
|
+
| 余额 | Novita | `api.novita.ai` |
|
|
65
|
+
|
|
66
|
+
订阅模式显示 `5h X% Wk Y%`。余额模式显示 `¥34.20` 或 `$5.88/$10.00`。未知 provider 会输出空字符串,不会污染 statusline。
|
|
67
|
+
|
|
68
|
+
## 显示格式
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cc-usage-bar install --format <preset> --bar-width <n>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| 预设 | 输出示例 |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `compact` 默认 | `5h 47% Wk 59%` |
|
|
77
|
+
| `numeric` | `47% / 59%` |
|
|
78
|
+
| `time` | `47% until 18:00 / 59% until 5/9 09:00` |
|
|
79
|
+
| `bar` | `[█████░░░░░] 47% / [██████░░░░] 59%` |
|
|
80
|
+
| `bar-time` | `[█████░░░░░] 47% until 18:00 / [██████░░░░] 59% until 5/9 09:00` |
|
|
81
|
+
|
|
82
|
+
时间使用本地时区,并做智能缩短:当天显示 `HH:MM`,一周内显示 `M/D HH:MM`,更久显示 `YYYY-MM-DD`。
|
|
83
|
+
|
|
84
|
+
颜色规则:5 小时窗口低于 60% 为绿色,低于 85% 为黄色,85% 及以上为红色;周用量 80% 及以上为加粗红色。设置 `NO_COLOR=1` 可以关闭颜色。
|
|
85
|
+
|
|
86
|
+
### 自定义模板
|
|
87
|
+
|
|
88
|
+
如果想完全控制文本,可以在 `~/.claude/settings.json` 的 `env` 里设置 `CC_USAGE_TEMPLATE`:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"env": {
|
|
93
|
+
"CC_USAGE_TEMPLATE": "{label}={percent}% ({expiry})"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
可用占位符:`{label}`、`{percent}`、`{bar}`、`{expiry}`、`{provider}`、`{amount}`。
|
|
99
|
+
|
|
100
|
+
### 自定义进度条
|
|
101
|
+
|
|
102
|
+
本项目提供一个适合 AI 使用的轻量入口:`--bar-spec` 或 `CC_USAGE_BAR_SPEC`。
|
|
103
|
+
|
|
104
|
+
工具本身不会调用 AI,也不会解析图片;它只负责渲染 JSON。你可以让自己的 AI 助手把图片、短语、主题转换成下面几种 spec。
|
|
105
|
+
|
|
106
|
+
#### tint:整段文字始终可见
|
|
107
|
+
|
|
108
|
+
推荐给颜文字、短句、logo 风格文本。已完成部分会染色,未完成部分会变暗,但整段文字一直可见:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
cc-usage-bar install --format=bar-time \
|
|
112
|
+
--bar-spec='{"mode":"tint","text":"Ciallo~(∠・ω< )⌒★"}'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{"mode":"tint","text":"Ciallo~(∠・ω< )⌒★"}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### cells:替换默认块字符
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{"mode":"cells","filled":"●","empty":"○","width":10}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### frames:按百分比选择阶段文本
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{"mode":"frames","frames":["(・_・)","(・ω・)","(∠・ω< )","Ciallo~(∠・ω< )⌒★"]}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
可以直接把下面这段发给你的 AI 助手:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
请把这张图片、这句话或这个主题转换成 cc-usage-bar 的 bar spec。
|
|
135
|
+
只返回一个 JSON 对象,mode 使用 "tint"、"cells" 或 "frames"。
|
|
136
|
+
短语、颜文字、logo、装饰文本优先使用 "tint",因为它会让整段文字始终可见,只按进度染色。
|
|
137
|
+
输出必须是单行、终端友好、适合放进 --bar-spec 的 JSON。
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 手动编辑 statusline
|
|
141
|
+
|
|
142
|
+
安装命令只是写入 `settings.json`,你可以手动调整:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
"statusLine": {
|
|
146
|
+
"type": "command",
|
|
147
|
+
"command": "cc-usage-fetch --format=bar-time --bar-width=15",
|
|
148
|
+
"refreshInterval": 30
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 诊断
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cc-usage-bar status
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
会显示当前 provider、数据来源、缓存状态、5 小时 / 7 天重置倒计时,或余额详情。该命令会跳过缓存,适合排查问题。
|
|
159
|
+
|
|
160
|
+
示例:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
provider: anthropic
|
|
164
|
+
source: api
|
|
165
|
+
cache status: miss
|
|
166
|
+
base url: <not set, defaults to anthropic>
|
|
167
|
+
5-hour: 47% (resets in 3h 19m)
|
|
168
|
+
7-day: 59% (resets in 9h 39m)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 常见问题
|
|
172
|
+
|
|
173
|
+
| 现象 | 原因 | 处理 |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| statusline 为空 | 免费用户没有 stdin `rate_limits`,也没有可用 token;或 provider 未识别 | 运行 `cc-usage-bar status`,检查 `ANTHROPIC_BASE_URL` |
|
|
176
|
+
| `unauthorized (401)` | OAuth token 过期,或 `ANTHROPIC_AUTH_TOKEN` 错误 | Anthropic 重新运行 `claude` 登录;其他 provider 更新 token |
|
|
177
|
+
| statusline 显示旧数据 | 成功结果有 30 秒缓存 | 等待,或删除 `/tmp/cc-oauth-usage.json` |
|
|
178
|
+
| 进度条显示成 `??` | 终端字体不支持 Unicode 块字符 | 使用 `--format=numeric`,或换支持 Unicode 的字体 |
|
|
179
|
+
|
|
180
|
+
## 隐私与 API 稳定性
|
|
181
|
+
|
|
182
|
+
Anthropic 订阅用量依赖未公开接口 `/api/oauth/usage`,并会从本机 macOS keychain 或 `~/.claude/.credentials.json` 读取 Claude Code OAuth token。这些接口可能变化。
|
|
183
|
+
|
|
184
|
+
Token 只在你的机器上读取,不会上传到本项目的任何服务。工具只会请求你配置的 provider。使用 stdin `rate_limits` 时不需要网络。
|
|
185
|
+
|
|
186
|
+
非 Anthropic provider 也是同样逻辑:`ANTHROPIC_AUTH_TOKEN` 只会发送给对应 provider 的余额或用量接口。
|
|
187
|
+
|
|
188
|
+
## 为什么不依赖 jq / curl
|
|
189
|
+
|
|
190
|
+
这个工具是纯 Node.js 实现,Node 18+ 自带 `fetch`,只在 macOS 读取 keychain 时使用 `child_process`。安装后直接可用,不需要额外安装 `jq` 或 `curl`。
|
|
191
|
+
|
|
192
|
+
## 致谢
|
|
193
|
+
|
|
194
|
+
Provider 适配逻辑,包括接口地址、响应解析和 OAuth token 发现流程,参考并移植自 [cc-switch](https://github.com/farion1231/cc-switch) 的 Rust 实现(`src-tauri/src/services/{subscription,coding_plan,balance}.rs`)。感谢该项目对国内 provider 集成的逆向和维护。
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const commander_1 = require("commander");
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const settings_1 = require("./settings");
|
|
40
|
+
const fetch_1 = require("./fetch");
|
|
41
|
+
const format_1 = require("./format");
|
|
42
|
+
function readPkgVersion() {
|
|
43
|
+
try {
|
|
44
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
45
|
+
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
46
|
+
return JSON.parse(raw).version ?? '0.0.0';
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return '0.0.0';
|
|
50
|
+
}
|
|
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
|
+
function loadSettingsOrExit() {
|
|
72
|
+
try {
|
|
73
|
+
return (0, settings_1.readSettings)();
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof settings_1.SettingsError) {
|
|
77
|
+
console.error(`error: ${e.message}`);
|
|
78
|
+
if (e.hint)
|
|
79
|
+
console.error(`hint: ${e.hint}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function runInstall(force, installOpts) {
|
|
86
|
+
if (!(0, settings_1.claudeDirExists)()) {
|
|
87
|
+
console.error('No ~/.claude/ directory found. Install Claude Code and run `claude` to log in first.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const settings = loadSettingsOrExit();
|
|
91
|
+
if ((0, settings_1.isInstalled)(settings) && !force) {
|
|
92
|
+
console.log('Already installed. Run `cc-usage-statusline uninstall` first, or pass --force to reinstall.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const backup = (0, settings_1.backupSettings)();
|
|
96
|
+
const next = (0, settings_1.applyInstall)(settings, installOpts);
|
|
97
|
+
(0, settings_1.writeSettings)(next);
|
|
98
|
+
console.log(`✓ Updated ${settings_1.SETTINGS_PATH}`);
|
|
99
|
+
if (backup)
|
|
100
|
+
console.log(`✓ Backup: ${backup}`);
|
|
101
|
+
process.stdout.write('Verifying… ');
|
|
102
|
+
const result = await (0, fetch_1.run)({ forceFresh: true, skipStdin: true });
|
|
103
|
+
if (result.data) {
|
|
104
|
+
const summary = result.data.kind === 'subscription'
|
|
105
|
+
? `5h ${Math.round(result.data.five_hour?.utilization ?? 0)}% / 7d ${Math.round(result.data.seven_day?.utilization ?? 0)}%`
|
|
106
|
+
: `${result.data.remaining.toFixed(2)} ${result.data.unit}`;
|
|
107
|
+
console.log(`OK [${result.adapterId}] ${summary}`);
|
|
108
|
+
console.log('Restart Claude Code to see the statusline.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log('FAIL');
|
|
112
|
+
if (!result.adapterId) {
|
|
113
|
+
console.error(' → No matching provider. Check ANTHROPIC_BASE_URL.');
|
|
114
|
+
}
|
|
115
|
+
else if (result.authFailed) {
|
|
116
|
+
console.error(' → Auth failed. Re-login with `claude` (Anthropic) or check ANTHROPIC_AUTH_TOKEN.');
|
|
117
|
+
}
|
|
118
|
+
else if (result.error) {
|
|
119
|
+
console.error(` → ${result.error}`);
|
|
120
|
+
}
|
|
121
|
+
console.error('(Settings were still updated. Run `cc-usage-statusline status` to retry diagnostics.)');
|
|
122
|
+
}
|
|
123
|
+
async function runUninstall() {
|
|
124
|
+
if (!(0, settings_1.claudeDirExists)()) {
|
|
125
|
+
console.log('Nothing to do — no ~/.claude/ directory.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const settings = loadSettingsOrExit();
|
|
129
|
+
if ((0, settings_1.isInstalled)(settings)) {
|
|
130
|
+
const latest = (0, settings_1.findLatestBackup)();
|
|
131
|
+
if (latest) {
|
|
132
|
+
(0, settings_1.restoreFromBackup)(latest);
|
|
133
|
+
const removed = (0, settings_1.deleteBackups)();
|
|
134
|
+
console.log(`✓ Restored from backup: ${latest}`);
|
|
135
|
+
console.log(`✓ Cleaned up ${removed} backup file(s).`);
|
|
136
|
+
console.log('Restart Claude Code for changes to take effect.');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const next = (0, settings_1.applyUninstallSurgical)(settings);
|
|
141
|
+
(0, settings_1.writeSettings)(next);
|
|
142
|
+
const removed = (0, settings_1.deleteBackups)();
|
|
143
|
+
console.log('✓ Removed cc-usage-fetch from statusLine (surgical mode).');
|
|
144
|
+
if (removed > 0)
|
|
145
|
+
console.log(`✓ Cleaned up ${removed} backup file(s).`);
|
|
146
|
+
console.log('Restart Claude Code for changes to take effect.');
|
|
147
|
+
}
|
|
148
|
+
async function runStatus() {
|
|
149
|
+
const result = await (0, fetch_1.run)({ forceFresh: true, skipStdin: true });
|
|
150
|
+
console.log(`provider: ${result.adapterId ?? '<none>'}`);
|
|
151
|
+
console.log(`source: ${result.source}`);
|
|
152
|
+
console.log(`cache status: ${result.cacheStatus}`);
|
|
153
|
+
console.log(`base url: ${process.env.ANTHROPIC_BASE_URL ?? '<not set, defaults to anthropic>'}`);
|
|
154
|
+
if (result.error)
|
|
155
|
+
console.log(`error: ${result.error}`);
|
|
156
|
+
if (result.authFailed) {
|
|
157
|
+
console.log('→ Auth failed. Re-login (Anthropic) or check ANTHROPIC_AUTH_TOKEN.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!result.data) {
|
|
161
|
+
if (!result.adapterId) {
|
|
162
|
+
console.log('→ Configure ANTHROPIC_BASE_URL to a supported provider, or leave unset for Anthropic.');
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (result.data.kind === 'subscription') {
|
|
167
|
+
const fh = result.data.five_hour;
|
|
168
|
+
const wk = result.data.seven_day;
|
|
169
|
+
if (result.data.planName)
|
|
170
|
+
console.log(`plan: ${result.data.planName}`);
|
|
171
|
+
if (fh)
|
|
172
|
+
console.log(`5-hour: ${Math.round(fh.utilization)}% (resets in ${formatCountdown(fh.resets_at)})`);
|
|
173
|
+
if (wk)
|
|
174
|
+
console.log(`7-day: ${Math.round(wk.utilization)}% (resets in ${formatCountdown(wk.resets_at)})`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const d = result.data;
|
|
178
|
+
if (d.planName)
|
|
179
|
+
console.log(`plan: ${d.planName}`);
|
|
180
|
+
console.log(`remaining: ${d.remaining.toFixed(2)} ${d.unit}`);
|
|
181
|
+
if (typeof d.total === 'number')
|
|
182
|
+
console.log(`total: ${d.total.toFixed(2)} ${d.unit}`);
|
|
183
|
+
if (typeof d.used === 'number')
|
|
184
|
+
console.log(`used: ${d.used.toFixed(2)} ${d.unit}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const program = new commander_1.Command();
|
|
188
|
+
program
|
|
189
|
+
.name('cc-usage-statusline')
|
|
190
|
+
.description('Show your Claude Code subscription usage in the statusline.')
|
|
191
|
+
.version(readPkgVersion());
|
|
192
|
+
program
|
|
193
|
+
.command('install')
|
|
194
|
+
.description('Install statusline integration into ~/.claude/settings.json')
|
|
195
|
+
.option('-f, --force', 'overwrite if already installed')
|
|
196
|
+
.option('--format <preset>', `render preset (${format_1.FORMAT_PRESETS.join(' | ')})`, 'compact')
|
|
197
|
+
.option('--bar-width <n>', 'bar width when format includes a bar (1-50)', '10')
|
|
198
|
+
.option('--bar-spec <json>', 'custom bar JSON spec (cells, tint, or frames)')
|
|
199
|
+
.action(async (opts) => {
|
|
200
|
+
const installOpts = {};
|
|
201
|
+
if (opts.format && opts.format !== 'compact') {
|
|
202
|
+
if (!(0, format_1.isValidPreset)(opts.format)) {
|
|
203
|
+
console.error(`error: --format must be one of ${format_1.FORMAT_PRESETS.join(', ')}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
installOpts.format = opts.format;
|
|
207
|
+
}
|
|
208
|
+
if (opts.barWidth) {
|
|
209
|
+
const n = parseInt(opts.barWidth, 10);
|
|
210
|
+
if (!Number.isFinite(n) || n < 1 || n > 50) {
|
|
211
|
+
console.error('error: --bar-width must be 1..50');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
if (n !== 10)
|
|
215
|
+
installOpts.barWidth = n;
|
|
216
|
+
}
|
|
217
|
+
if (opts.barSpec) {
|
|
218
|
+
if (!(0, format_1.parseBarSpec)(opts.barSpec)) {
|
|
219
|
+
console.error('error: --bar-spec must be valid cells, tint, or frames JSON');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
installOpts.barSpec = opts.barSpec;
|
|
223
|
+
}
|
|
224
|
+
await runInstall(!!opts.force, installOpts);
|
|
225
|
+
});
|
|
226
|
+
program
|
|
227
|
+
.command('uninstall')
|
|
228
|
+
.description('Remove statusline integration; restore from backup if possible')
|
|
229
|
+
.action(async () => {
|
|
230
|
+
await runUninstall();
|
|
231
|
+
});
|
|
232
|
+
program
|
|
233
|
+
.command('status')
|
|
234
|
+
.description('Diagnose: token source, current usage, reset countdowns, cache state')
|
|
235
|
+
.action(async () => {
|
|
236
|
+
await runStatus();
|
|
237
|
+
});
|
|
238
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
239
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|