cc-api-statusline 1.0.1 → 1.1.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.md +32 -32
- package/README.zh-CN.md +295 -0
- package/dist/cc-api-statusline.js +351 -153
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# cc-api-statusline
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
<img src="docs/images/banner-screenshot.png" width="800" alt="cc-api-statusline banner">
|
|
6
|
+
|
|
7
|
+
A high-performance TUI statusline tool that polls API usage data from Claude API services (sub2api, claude-relay-service, or custom providers) and renders a configurable one-line status display.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
|
-
- ⚡ **Fast piped mode** — <25ms warm cache, <100ms p95
|
|
8
11
|
- 🎨 **Highly configurable** — Layouts, colors, bar styles, display modes
|
|
9
12
|
- 🔌 **Provider autodetection** — Works with sub2api, claude-relay-service, custom providers
|
|
10
|
-
- 💾 **Smart caching** — Disk cache with atomic writes, TTL validation, automatic garbage collection
|
|
11
|
-
- 🎯 **Claude Code integration** — Auto-setup with `--install` command
|
|
12
13
|
- 📊 **Multiple components** — Daily/weekly/monthly quotas, balance, tokens, rate limits
|
|
13
14
|
- 🔁 **Hot switching** — Auto-detects API endpoint and credential changes at runtime
|
|
14
15
|
- 🔒 **Reliability** — No stale data display, race-condition-free writes, auto cache cleanup
|
|
@@ -43,7 +44,7 @@ export ANTHROPIC_AUTH_TOKEN="your-api-token"
|
|
|
43
44
|
bunx cc-api-statusline@latest --once
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
### 3. Install as Claude Code widget
|
|
47
|
+
### 3.a Install as Claude Code widget
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
50
|
bunx cc-api-statusline@latest --install
|
|
@@ -54,6 +55,7 @@ This adds to `~/.claude/settings.json`:
|
|
|
54
55
|
```json
|
|
55
56
|
{
|
|
56
57
|
"statusLine": {
|
|
58
|
+
"id": "e62435aa-bdf5-4ded-a2e3-ae13582439db",
|
|
57
59
|
"type": "command",
|
|
58
60
|
"command": "bunx -y cc-api-statusline@latest",
|
|
59
61
|
"padding": 0
|
|
@@ -61,6 +63,28 @@ This adds to `~/.claude/settings.json`:
|
|
|
61
63
|
}
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
### 3.b Install as [ccstatusline](https://github.com/anthropics/claude-code) Custom Command
|
|
67
|
+
<img src="docs/images/ccstatusline-command.png" width="800" alt="ccstatusline-command mode">
|
|
68
|
+
Add to `~/.claude/ccstatusline/config.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"lines": [
|
|
73
|
+
[
|
|
74
|
+
{
|
|
75
|
+
"id": "e62435aa-bdf5-4ded-a2e3-ae13582439db",
|
|
76
|
+
"type": "custom-command",
|
|
77
|
+
"commandPath": "bunx -y cc-api-statusline@latest --embedded",
|
|
78
|
+
"preserveColors": true,
|
|
79
|
+
"timeout": 10000
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> **`--embedded` is required here.** Without it, cc-api-statusline prepends an ANSI reset (`\x1b[0m`) that breaks cc-statusline's powerline background colors. The flag tells cc-api-statusline it's running inside a host renderer that handles its own formatting.
|
|
87
|
+
|
|
64
88
|
Using `bunx` ensures you always run the latest version without a global install. To uninstall:
|
|
65
89
|
|
|
66
90
|
```bash
|
|
@@ -142,31 +166,6 @@ cc-api-statusline --apply-config
|
|
|
142
166
|
|
|
143
167
|
See [docs/api-config-reference.md](docs/api-config-reference.md) for the full schema.
|
|
144
168
|
|
|
145
|
-
## [ccstatusline](https://github.com/anthropics/claude-code) Custom Command
|
|
146
|
-
|
|
147
|
-
Add to `~/.claude/ccstatusline/config.json`:
|
|
148
|
-
|
|
149
|
-
```json
|
|
150
|
-
{
|
|
151
|
-
"customCommands": {
|
|
152
|
-
"usage": {
|
|
153
|
-
"command": "cc-api-statusline",
|
|
154
|
-
"description": "API usage statusline",
|
|
155
|
-
"type": "piped"
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
"widgets": [
|
|
159
|
-
{
|
|
160
|
-
"type": "customCommand",
|
|
161
|
-
"command": "usage",
|
|
162
|
-
"refreshIntervalMs": 30000,
|
|
163
|
-
"maxWidth": 100,
|
|
164
|
-
"preserveColors": true
|
|
165
|
-
}
|
|
166
|
-
]
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
169
|
## Environment Variables
|
|
171
170
|
|
|
172
171
|
All variables are optional at the shell level — `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` can be set via `settings.json` env overlay instead of shell exports (see [Quick Start](#quick-start)).
|
|
@@ -177,7 +176,8 @@ All variables are optional at the shell level — `ANTHROPIC_BASE_URL` and `ANTH
|
|
|
177
176
|
| `ANTHROPIC_AUTH_TOKEN` | Yes | API key or token |
|
|
178
177
|
| `CC_STATUSLINE_PROVIDER` | Yes | Override provider detection (`sub2api`, `claude-relay-service`, or custom) |
|
|
179
178
|
| `CC_STATUSLINE_POLL` | Yes | Override poll interval (seconds, min 5) |
|
|
180
|
-
| `CC_STATUSLINE_TIMEOUT` | Yes | Piped mode timeout (milliseconds, default
|
|
179
|
+
| `CC_STATUSLINE_TIMEOUT` | Yes | Piped mode timeout (milliseconds, default 5000) |
|
|
180
|
+
| `CC_API_STATUSLINE_EMBEDDED` | Yes | Skip host formatting when set to `"1"` or `"true"`. Alternative to `--embedded` flag; prefer the flag in `commandPath` configs |
|
|
181
181
|
| `DEBUG` or `CC_STATUSLINE_DEBUG` | Yes | Enable debug logging to `~/.claude/cc-api-statusline/debug.log` |
|
|
182
182
|
|
|
183
183
|
## Troubleshooting
|
|
@@ -219,7 +219,7 @@ cc-api-statusline --once
|
|
|
219
219
|
DEBUG=1 cc-api-statusline --once
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
-
Verify `pipedRequestTimeoutMs` in config (default
|
|
222
|
+
Verify `pipedRequestTimeoutMs` in config (default 3000ms) and check `~/.claude/cc-api-statusline/cache-*.json` exists.
|
|
223
223
|
|
|
224
224
|
### Widget shows `[Exit: 1]` in Claude Code
|
|
225
225
|
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# cc-api-statusline
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
在ClaudeCode状态栏显示API用量,通过轮询 Claude API 服务(sub2api、claude-relay-service 或自定义提供商)获取用量数据,并以可配置显示样式。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- 🎨 **高度可配置** — 布局、颜色、进度条样式、显示模式任意调整
|
|
10
|
+
- 🔌 **提供商自动识别** — 开箱支持 sub2api、claude-relay-service 及自定义提供商
|
|
11
|
+
- 🎯 **Claude Code 集成** — 一键 `--install` 完成安装
|
|
12
|
+
- 📊 **多维度用量展示** — 每日/每周/每月配额、余额、Token数、速率限制
|
|
13
|
+
- 🔁 **热切换** — 自动感知 API 端点和凭证变更,无需重启
|
|
14
|
+
- 🔒 **高可靠性** — 无过期数据展示、无竞争条件写入、缓存自动清理
|
|
15
|
+
|
|
16
|
+
## 快速上手
|
|
17
|
+
|
|
18
|
+
### 1. 配置 API 端点
|
|
19
|
+
|
|
20
|
+
需要准备 `ANTHROPIC_BASE_URL`(代理地址)和 `ANTHROPIC_AUTH_TOKEN`(API 密钥)两个变量。
|
|
21
|
+
|
|
22
|
+
**推荐方式:写入 `~/.claude/settings.json` 的 env 字段**(会自动传递给组件):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"env": {
|
|
27
|
+
"ANTHROPIC_BASE_URL": "https://your-proxy.example.com",
|
|
28
|
+
"ANTHROPIC_AUTH_TOKEN": "your-api-token"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
也可以直接在 Shell 中导出:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export ANTHROPIC_BASE_URL="https://your-proxy.example.com"
|
|
37
|
+
export ANTHROPIC_AUTH_TOKEN="your-api-token"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. 预览效果
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bunx cc-api-statusline@latest --once
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3.a 安装为 Claude Code 状态栏组件
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bunx cc-api-statusline@latest --install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
此命令会自动向 `~/.claude/settings.json` 写入以下配置:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"statusLine": {
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "bunx -y cc-api-statusline@latest",
|
|
59
|
+
"padding": 0
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
### 3.b 安装为 [ccstatusline](https://github.com/anthropics/claude-code) 自定义命令
|
|
64
|
+
<img src="docs/images/ccstatusline-command.png" width="800" alt="ccstatusline-command mode">
|
|
65
|
+
在 `~/.claude/ccstatusline/config.json` 中添加如下配置:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"lines": [
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
"id": "e62435aa-bdf5-4ded-a2e3-ae13582439db",
|
|
73
|
+
"type": "custom-command",
|
|
74
|
+
"commandPath": "bunx -y cc-api-statusline@latest --embedded",
|
|
75
|
+
"preserveColors": true,
|
|
76
|
+
"timeout": 10000
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> **此处必须加 `--embedded`。** 不加的话,cc-api-statusline 会在输出前插入 ANSI 重置码(`\x1b[0m`),破坏 cc-statusline 的 powerline 背景色。该标志告知 cc-api-statusline 当前运行在宿主渲染器内部,由宿主负责格式化。
|
|
84
|
+
|
|
85
|
+
使用 `bunx` 可每次自动拉取最新版本,无需全局安装。如需卸载:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bunx cc-api-statusline --uninstall
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
也支持全局安装:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bun add -g cc-api-statusline
|
|
95
|
+
# 或
|
|
96
|
+
npm install -g cc-api-statusline
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 热切换
|
|
100
|
+
|
|
101
|
+
当 `ANTHROPIC_BASE_URL` 或 `ANTHROPIC_AUTH_TOKEN` 发生变化时(例如切换 Claude Code 配置文件或轮换密钥),cc-api-statusline 会自动检测并触发切换。切换期间会短暂显示过渡提示(`⟳ Switching provider...`),随后从新端点刷新数据,全程无需重启。
|
|
102
|
+
|
|
103
|
+
## 配置
|
|
104
|
+
|
|
105
|
+
### 样式配置(`~/.claude/cc-api-statusline/config.json`)
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"display": {
|
|
110
|
+
"layout": "standard",
|
|
111
|
+
"displayMode": "text",
|
|
112
|
+
"progressStyle": "icon",
|
|
113
|
+
"barStyle": "block",
|
|
114
|
+
"divider": { "text": "|", "margin": 1, "color": "#555753" },
|
|
115
|
+
"maxWidth": 100
|
|
116
|
+
},
|
|
117
|
+
"components": {
|
|
118
|
+
"daily": true,
|
|
119
|
+
"weekly": true,
|
|
120
|
+
"monthly": true,
|
|
121
|
+
"balance": true,
|
|
122
|
+
"tokens": false,
|
|
123
|
+
"rateLimit": false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
主要配置项说明:
|
|
129
|
+
|
|
130
|
+
| 配置项 | 可选值 | 默认值 | 说明 |
|
|
131
|
+
|--------|--------|--------|------|
|
|
132
|
+
| `layout` | `standard` / `percent-first` | `standard` | 标签、进度条、数值的排列顺序 |
|
|
133
|
+
| `displayMode` | `text` / `compact` / `emoji` / `nerd` / `hidden` | `text` | 标签样式。`nerd` 需安装 [Nerd Font](https://www.nerdfonts.com/font-downloads) |
|
|
134
|
+
| `progressStyle` | `bar` / `icon` / `hidden` | `icon` | 用量进度的可视化方式。`icon` 需安装 [Nerd Font](https://www.nerdfonts.com/font-downloads) |
|
|
135
|
+
| `barStyle` | `block` / `classic` / `dot` / `shade` / `pipe` / `braille` / `square` / `star` | `block` | 进度条字符样式 |
|
|
136
|
+
| `barSize` | `small` / `small-medium` / `medium` / `medium-large` / `large` | `medium` | 进度条宽度(4–12 个字符) |
|
|
137
|
+
| `divider` | `DividerConfig` 或 `false` | `{ text: "\|", margin: 1, color: "#555753" }` | 组件间分隔符;设为 `false` 可禁用 |
|
|
138
|
+
| `maxWidth` | 20–100 | `100` | 状态栏最大宽度占终端宽度的百分比 |
|
|
139
|
+
|
|
140
|
+
完整样式参考(包含每组件独立配置、颜色别名、倒计时等高级选项)请查阅 [docs/spec-tui-style.md](docs/spec-tui-style.md)。
|
|
141
|
+
|
|
142
|
+
#### User-Agent 伪装
|
|
143
|
+
|
|
144
|
+
部分提供商会限制非 Claude Code 客户端的访问,可启用此选项绕过:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"spoofClaudeCodeUA": true
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `false` / `undefined` — 不发送 User-Agent 请求头(默认)
|
|
153
|
+
- `true` — 自动获取当前 Claude Code 版本,获取失败则回退至 `claude-cli/2.1.56 (external, cli)`
|
|
154
|
+
- `"string"` — 使用指定的自定义 User-Agent 字符串
|
|
155
|
+
|
|
156
|
+
### API 提供商配置(`~/.claude/cc-api-statusline/api-config/`)
|
|
157
|
+
|
|
158
|
+
在此目录下以 JSON 文件形式定义自定义提供商。添加或修改后,执行以下命令使其生效:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
cc-api-statusline --apply-config
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
完整 Schema 请参阅 [docs/api-config-reference.md](docs/api-config-reference.md)。
|
|
165
|
+
|
|
166
|
+
## 环境变量
|
|
167
|
+
|
|
168
|
+
以下所有变量均为可选——`ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 可通过 `settings.json` 的 env 字段配置,无需在 Shell 中手动导出(详见[快速上手](#快速上手))。
|
|
169
|
+
|
|
170
|
+
| 变量 | 是否可选 | 说明 |
|
|
171
|
+
|------|----------|------|
|
|
172
|
+
| `ANTHROPIC_BASE_URL` | 是 | API 端点地址(如 `https://api.sub2api.com`) |
|
|
173
|
+
| `ANTHROPIC_AUTH_TOKEN` | 是 | API 密钥或Token |
|
|
174
|
+
| `CC_STATUSLINE_PROVIDER` | 是 | 手动指定提供商(`sub2api`、`claude-relay-service` 或自定义) |
|
|
175
|
+
| `CC_STATUSLINE_POLL` | 是 | 轮询间隔(秒,最小 5) |
|
|
176
|
+
| `CC_STATUSLINE_TIMEOUT` | 是 | 管道模式超时时间(毫秒,默认 5000) |
|
|
177
|
+
| `DEBUG` 或 `CC_STATUSLINE_DEBUG` | 是 | 开启调试日志,输出至 `~/.claude/cc-api-statusline/debug.log` |
|
|
178
|
+
|
|
179
|
+
## 常见问题
|
|
180
|
+
|
|
181
|
+
### 提示 "Missing required environment variable"
|
|
182
|
+
|
|
183
|
+
请通过 Shell 导出或 `settings.json` env 字段设置 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN`(详见[快速上手](#快速上手))。
|
|
184
|
+
|
|
185
|
+
### 提示 "Unknown provider"
|
|
186
|
+
|
|
187
|
+
提供商自动识别失败,请手动指定:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
export CC_STATUSLINE_PROVIDER="sub2api"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
或在 `api-config/` 目录下定义自定义提供商。
|
|
194
|
+
|
|
195
|
+
### 显示 "[offline]" 或 "[stale]"
|
|
196
|
+
|
|
197
|
+
通常由网络错误或缓存过期导致,可开启调试日志排查:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
DEBUG=1 cc-api-statusline --once
|
|
201
|
+
tail -f ~/.claude/cc-api-statusline/debug.log
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
排查要点:
|
|
205
|
+
- `ANTHROPIC_BASE_URL` 对应的网络是否可达
|
|
206
|
+
- API 端点是否正常响应
|
|
207
|
+
- Token是否有效且未过期
|
|
208
|
+
|
|
209
|
+
### 管道模式响应较慢
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# 单独预热缓存
|
|
213
|
+
cc-api-statusline --once
|
|
214
|
+
# 查看详细耗时
|
|
215
|
+
DEBUG=1 cc-api-statusline --once
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
检查配置中的 `pipedRequestTimeoutMs`(默认 3000ms),并确认 `~/.claude/cc-api-statusline/cache-*.json` 文件已存在。
|
|
219
|
+
|
|
220
|
+
### Claude Code 中组件显示 `[Exit: 1]`
|
|
221
|
+
|
|
222
|
+
在 `~/.claude/settings.json` 中开启调试日志:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"statusLine": {
|
|
227
|
+
"type": "command",
|
|
228
|
+
"command": "DEBUG=1 bunx -y cc-api-statusline@latest",
|
|
229
|
+
"padding": 0
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
然后查看日志:`tail -f ~/.claude/cc-api-statusline/debug.log`
|
|
235
|
+
|
|
236
|
+
## 开发
|
|
237
|
+
|
|
238
|
+
| 命令 | 说明 |
|
|
239
|
+
|------|------|
|
|
240
|
+
| `bun install` | 安装依赖 |
|
|
241
|
+
| `bun run start` | 单次获取(--once 模式),用于快速调试 |
|
|
242
|
+
| `bun run example` | 模拟管道模式 |
|
|
243
|
+
| `bun run test` | 运行测试 |
|
|
244
|
+
| `bun run lint` | 代码检查 |
|
|
245
|
+
| `bun run build` | 构建 |
|
|
246
|
+
| `bun run check` | 运行全部检查 |
|
|
247
|
+
|
|
248
|
+
### 调试日志
|
|
249
|
+
|
|
250
|
+
启用详细执行日志:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# 开启调试
|
|
254
|
+
DEBUG=1 cc-api-statusline --once
|
|
255
|
+
|
|
256
|
+
# 用于 Claude Code 组件时,在 settings.json 中设置:
|
|
257
|
+
# "command": "DEBUG=1 bunx -y cc-api-statusline@latest"
|
|
258
|
+
|
|
259
|
+
# 实时追踪日志
|
|
260
|
+
tail -f ~/.claude/cc-api-statusline/debug.log
|
|
261
|
+
|
|
262
|
+
# 搜索错误记录
|
|
263
|
+
grep "ERROR" ~/.claude/cc-api-statusline/debug.log
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
调试日志涵盖:执行时间戳、模式检测、配置与缓存状态、执行路径(A/B/C/D)、请求耗时及错误详情。
|
|
267
|
+
|
|
268
|
+
日志文件自动轮转(约每 20 次调用触发一次):
|
|
269
|
+
- `debug.log` ≥ 500 KB → 归档为 `debug.YYYY-MM-DDTHH-MM.log`
|
|
270
|
+
- 归档超过 24 小时 → gzip 压缩
|
|
271
|
+
- 压缩归档超过 3 天 → 自动删除
|
|
272
|
+
|
|
273
|
+
## 测试
|
|
274
|
+
|
|
275
|
+
- **691 个测试**,覆盖 **39 个测试文件**
|
|
276
|
+
- 所有服务、渲染器及工具函数的单元测试
|
|
277
|
+
- 核心执行路径测试(A/B/C/D)
|
|
278
|
+
- 隔离环境端到端冒烟测试
|
|
279
|
+
- 性能测试(验证 p95 < 600ms)
|
|
280
|
+
- 缓存垃圾回收测试
|
|
281
|
+
- GitHub Actions CI/CD 流水线
|
|
282
|
+
|
|
283
|
+
运行:`bun run check`
|
|
284
|
+
|
|
285
|
+
## 许可证
|
|
286
|
+
|
|
287
|
+
MIT
|
|
288
|
+
|
|
289
|
+
## 相关文档
|
|
290
|
+
|
|
291
|
+
- [实现手册](docs/implementation-handbook.md)
|
|
292
|
+
- [当前实现说明](docs/current-implementation.md)
|
|
293
|
+
- [TUI 样式规范](docs/spec-tui-style.md)
|
|
294
|
+
- [API 轮询规范](docs/spec-api-polling.md)
|
|
295
|
+
- [自定义提供商规范](docs/spec-custom-providers.md)
|
|
@@ -4,7 +4,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
4
4
|
// package.json
|
|
5
5
|
var package_default = {
|
|
6
6
|
name: "cc-api-statusline",
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.1.1",
|
|
8
8
|
description: "Claude Code statusline tool that polls API usage from third-party proxy backends",
|
|
9
9
|
type: "module",
|
|
10
10
|
bin: {
|
|
@@ -70,6 +70,7 @@ function parseArgs() {
|
|
|
70
70
|
let uninstall = false;
|
|
71
71
|
let applyConfig = false;
|
|
72
72
|
let force = false;
|
|
73
|
+
let embedded = false;
|
|
73
74
|
let configPath;
|
|
74
75
|
let runner;
|
|
75
76
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -88,6 +89,8 @@ function parseArgs() {
|
|
|
88
89
|
applyConfig = true;
|
|
89
90
|
} else if (arg === "--force") {
|
|
90
91
|
force = true;
|
|
92
|
+
} else if (arg === "--embedded") {
|
|
93
|
+
embedded = true;
|
|
91
94
|
} else if (arg === "--config" && i + 1 < args.length) {
|
|
92
95
|
configPath = args[i + 1];
|
|
93
96
|
i++;
|
|
@@ -99,7 +102,9 @@ function parseArgs() {
|
|
|
99
102
|
i++;
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
|
-
|
|
105
|
+
const envVal = process.env["CC_API_STATUSLINE_EMBEDDED"];
|
|
106
|
+
embedded = embedded || envVal === "1" || envVal === "true";
|
|
107
|
+
return { help, version, once, install, uninstall, applyConfig, force, embedded, configPath, runner };
|
|
103
108
|
}
|
|
104
109
|
function showHelp() {
|
|
105
110
|
console.log(`
|
|
@@ -118,14 +123,16 @@ Options:
|
|
|
118
123
|
--apply-config Apply endpoint config changes (updates lock file, clears caches)
|
|
119
124
|
--runner <runner> Package runner: npx or bunx (default: auto-detect)
|
|
120
125
|
--force Force overwrite existing statusline configuration
|
|
126
|
+
--embedded Skip host formatting (for use inside cc-statusline)
|
|
121
127
|
|
|
122
128
|
Environment Variables:
|
|
123
|
-
ANTHROPIC_BASE_URL
|
|
124
|
-
ANTHROPIC_AUTH_TOKEN
|
|
125
|
-
CC_STATUSLINE_PROVIDER
|
|
126
|
-
CC_STATUSLINE_POLL
|
|
127
|
-
CC_STATUSLINE_TIMEOUT
|
|
128
|
-
DEBUG or CC_STATUSLINE_DEBUG
|
|
129
|
+
ANTHROPIC_BASE_URL API endpoint (required)
|
|
130
|
+
ANTHROPIC_AUTH_TOKEN API key (required)
|
|
131
|
+
CC_STATUSLINE_PROVIDER Override provider detection
|
|
132
|
+
CC_STATUSLINE_POLL Override poll interval (seconds)
|
|
133
|
+
CC_STATUSLINE_TIMEOUT Piped mode timeout (milliseconds, default 5000)
|
|
134
|
+
DEBUG or CC_STATUSLINE_DEBUG Enable debug logging to ~/.claude/cc-api-statusline/debug.log
|
|
135
|
+
CC_API_STATUSLINE_EMBEDDED Skip host formatting when set to "1" or "true" (for use inside cc-statusline)
|
|
129
136
|
|
|
130
137
|
Config File:
|
|
131
138
|
~/.claude/cc-api-statusline/config.json
|
|
@@ -177,8 +184,10 @@ import { spawn } from "child_process";
|
|
|
177
184
|
import { dirname, join } from "path";
|
|
178
185
|
|
|
179
186
|
// src/core/constants.ts
|
|
180
|
-
var
|
|
187
|
+
var DEFAULT_TIMEOUT_BUDGET_MS = 5000;
|
|
188
|
+
var TTY_TIMEOUT_BUDGET_MS = DEFAULT_TIMEOUT_BUDGET_MS * 2;
|
|
181
189
|
var EXIT_BUFFER_MS = 50;
|
|
190
|
+
var TIMEOUT_HEADROOM_MS = 100;
|
|
182
191
|
var STALENESS_THRESHOLD_MINUTES = 5;
|
|
183
192
|
var VERY_STALE_THRESHOLD_MINUTES = 30;
|
|
184
193
|
var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -189,6 +198,12 @@ var LOG_ROTATION_PROBABILITY = 0.05;
|
|
|
189
198
|
var LOG_MAX_SIZE_BYTES = 512 * 1024;
|
|
190
199
|
var LOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
191
200
|
var LOG_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
|
|
201
|
+
var HEALTH_MATCH_WILDCARD = "*";
|
|
202
|
+
var DETECTION_TTL_BASE_S = 86400;
|
|
203
|
+
var DETECTION_TTL_MAX_S = 604800;
|
|
204
|
+
var DETECTION_TTL_CHANGED_S = 3600;
|
|
205
|
+
var DETECTION_TTL_FAILED_S = 300;
|
|
206
|
+
var MAINTENANCE_GC_PROBABILITY = 0.1;
|
|
192
207
|
|
|
193
208
|
// src/services/log-rotator.ts
|
|
194
209
|
var ARCHIVE_LOG_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log$/;
|
|
@@ -563,7 +578,7 @@ var DEFAULT_CONFIG = {
|
|
|
563
578
|
chill: { tiers: buildTiers(["cyan", "cyan", "blue", "blue", "magenta"]) }
|
|
564
579
|
},
|
|
565
580
|
pollIntervalSeconds: 30,
|
|
566
|
-
pipedRequestTimeoutMs:
|
|
581
|
+
pipedRequestTimeoutMs: DEFAULT_TIMEOUT_BUDGET_MS
|
|
567
582
|
};
|
|
568
583
|
var BAR_SIZE_MAP = {
|
|
569
584
|
small: 4,
|
|
@@ -635,12 +650,11 @@ function isCacheEntry(value) {
|
|
|
635
650
|
const c = value;
|
|
636
651
|
return typeof c["version"] === "number" && typeof c["provider"] === "string" && typeof c["baseUrl"] === "string" && typeof c["tokenHash"] === "string" && typeof c["configHash"] === "string" && typeof c["endpointConfigHash"] === "string" && typeof c["data"] === "object" && c["data"] !== null && typeof c["renderedLine"] === "string" && typeof c["fetchedAt"] === "string" && typeof c["ttlSeconds"] === "number" && (c["errorState"] === null || typeof c["errorState"] === "object");
|
|
637
652
|
}
|
|
638
|
-
var PROVIDER_DETECTION_TTL_SECONDS = 86400;
|
|
639
653
|
function isProviderDetectionCacheEntry(value) {
|
|
640
654
|
if (typeof value !== "object" || value === null)
|
|
641
655
|
return false;
|
|
642
656
|
const c = value;
|
|
643
|
-
return typeof c["baseUrl"] === "string" && typeof c["provider"] === "string" && (c["detectedVia"] === "health-probe" || c["detectedVia"] === "
|
|
657
|
+
return typeof c["baseUrl"] === "string" && typeof c["provider"] === "string" && (c["detectedVia"] === "health-probe" || c["detectedVia"] === "override") && typeof c["detectedAt"] === "string" && typeof c["ttlSeconds"] === "number";
|
|
644
658
|
}
|
|
645
659
|
// src/services/endpoint-config.ts
|
|
646
660
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
@@ -813,7 +827,6 @@ function getBuiltInEndpointConfigs() {
|
|
|
813
827
|
resetSemantics: "rolling-window"
|
|
814
828
|
},
|
|
815
829
|
detection: {
|
|
816
|
-
urlPatterns: ["/apistats", "/api/user-stats"],
|
|
817
830
|
healthMatch: { service: "*" }
|
|
818
831
|
},
|
|
819
832
|
responseMapping: {
|
|
@@ -998,7 +1011,15 @@ function writeDefaultConfigs(customDir) {
|
|
|
998
1011
|
ensureDir(apiConfigDir);
|
|
999
1012
|
if (!existsSync4(configPath)) {
|
|
1000
1013
|
const styleConfigWithoutColors = serializableConfig(getDefaultStyleConfig());
|
|
1001
|
-
|
|
1014
|
+
const autoColorEntry = DEFAULT_CONFIG.colors?.auto;
|
|
1015
|
+
if (!autoColorEntry || typeof autoColorEntry === "string") {
|
|
1016
|
+
throw new Error("DEFAULT_CONFIG is missing the built-in auto color alias");
|
|
1017
|
+
}
|
|
1018
|
+
const configWithAutoColor = {
|
|
1019
|
+
...styleConfigWithoutColors,
|
|
1020
|
+
colors: { auto: { tiers: autoColorEntry.tiers } }
|
|
1021
|
+
};
|
|
1022
|
+
atomicWriteFile(configPath, JSON.stringify(configWithAutoColor, null, 2), {
|
|
1002
1023
|
appendNewline: true
|
|
1003
1024
|
});
|
|
1004
1025
|
}
|
|
@@ -1090,7 +1111,7 @@ async function readBodyWithLimit(response) {
|
|
|
1090
1111
|
throw new HttpError(`Failed to read response body: ${error}`);
|
|
1091
1112
|
}
|
|
1092
1113
|
}
|
|
1093
|
-
async function secureFetch(url, options = {}, timeoutMs =
|
|
1114
|
+
async function secureFetch(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS, userAgent) {
|
|
1094
1115
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
1095
1116
|
const fetchOptions = {
|
|
1096
1117
|
...options,
|
|
@@ -1139,7 +1160,33 @@ function extractOrigin(baseUrl) {
|
|
|
1139
1160
|
return baseUrl;
|
|
1140
1161
|
}
|
|
1141
1162
|
}
|
|
1142
|
-
|
|
1163
|
+
function matchHealthResponse(data, endpointConfigs) {
|
|
1164
|
+
const candidates = Object.entries(endpointConfigs).reduce((acc, [providerId, config]) => {
|
|
1165
|
+
const healthMatch = config.detection?.healthMatch;
|
|
1166
|
+
if (healthMatch != null && Object.keys(healthMatch).length > 0) {
|
|
1167
|
+
acc.push({ providerId, healthMatch });
|
|
1168
|
+
}
|
|
1169
|
+
return acc;
|
|
1170
|
+
}, []);
|
|
1171
|
+
candidates.sort((a, b) => {
|
|
1172
|
+
const diff = Object.keys(b.healthMatch).length - Object.keys(a.healthMatch).length;
|
|
1173
|
+
return diff !== 0 ? diff : a.providerId.localeCompare(b.providerId);
|
|
1174
|
+
});
|
|
1175
|
+
for (const { providerId, healthMatch } of candidates) {
|
|
1176
|
+
const matches = Object.entries(healthMatch).every(([field, expected]) => {
|
|
1177
|
+
const actual = data[field];
|
|
1178
|
+
if (expected === HEALTH_MATCH_WILDCARD) {
|
|
1179
|
+
return typeof actual === "string";
|
|
1180
|
+
}
|
|
1181
|
+
return actual === expected;
|
|
1182
|
+
});
|
|
1183
|
+
if (matches) {
|
|
1184
|
+
return providerId;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
async function probeHealth(baseUrl, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS, endpointConfigs = {}) {
|
|
1143
1190
|
const origin = extractOrigin(baseUrl);
|
|
1144
1191
|
const healthUrl = `${origin}/health`;
|
|
1145
1192
|
logger.debug("Probing health endpoint", { healthUrl, timeoutMs });
|
|
@@ -1152,13 +1199,10 @@ async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
|
1152
1199
|
}, timeoutMs);
|
|
1153
1200
|
const data = JSON.parse(responseText);
|
|
1154
1201
|
logger.debug("Health probe response", { data });
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (data["status"] === "ok") {
|
|
1160
|
-
logger.debug("Detected sub2api from status: ok pattern");
|
|
1161
|
-
return "sub2api";
|
|
1202
|
+
const matched = matchHealthResponse(data, endpointConfigs);
|
|
1203
|
+
if (matched) {
|
|
1204
|
+
logger.debug("Detected provider from health response", { provider: matched });
|
|
1205
|
+
return matched;
|
|
1162
1206
|
}
|
|
1163
1207
|
logger.debug("Health probe returned unrecognized pattern", { data });
|
|
1164
1208
|
return null;
|
|
@@ -1167,6 +1211,15 @@ async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
|
1167
1211
|
return null;
|
|
1168
1212
|
}
|
|
1169
1213
|
}
|
|
1214
|
+
async function probeHealthWithMetrics(baseUrl, timeoutMs, endpointConfigs) {
|
|
1215
|
+
const start = Date.now();
|
|
1216
|
+
const matchedProvider = await probeHealth(baseUrl, timeoutMs, endpointConfigs);
|
|
1217
|
+
return {
|
|
1218
|
+
success: matchedProvider !== null,
|
|
1219
|
+
matchedProvider,
|
|
1220
|
+
responseTimeMs: Date.now() - start
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1170
1223
|
|
|
1171
1224
|
// src/services/cache.ts
|
|
1172
1225
|
import { readFileSync as readFileSync6, unlinkSync as unlinkSync4 } from "fs";
|
|
@@ -1289,6 +1342,37 @@ function readProviderDetectionCache(baseUrl) {
|
|
|
1289
1342
|
return null;
|
|
1290
1343
|
}
|
|
1291
1344
|
}
|
|
1345
|
+
function deleteProviderDetectionCache(baseUrl) {
|
|
1346
|
+
const path = getProviderDetectionCachePath(baseUrl);
|
|
1347
|
+
try {
|
|
1348
|
+
unlinkSync4(path);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
logger.warn(`Failed to delete provider detection cache at ${path}: ${err}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function readDetectionCacheMeta(baseUrl) {
|
|
1357
|
+
const defaultTtlMs = DETECTION_TTL_BASE_S * 1000;
|
|
1358
|
+
const path = getProviderDetectionCachePath(baseUrl);
|
|
1359
|
+
let content;
|
|
1360
|
+
try {
|
|
1361
|
+
content = readFileSync6(path, "utf-8");
|
|
1362
|
+
} catch {
|
|
1363
|
+
return { ageMs: null, ttlMs: defaultTtlMs };
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const data = JSON.parse(content);
|
|
1367
|
+
if (!isProviderDetectionCacheEntry(data))
|
|
1368
|
+
return { ageMs: null, ttlMs: defaultTtlMs };
|
|
1369
|
+
const detectedAt = new Date(data.detectedAt).getTime();
|
|
1370
|
+
const ageMs = isNaN(detectedAt) ? null : Date.now() - detectedAt;
|
|
1371
|
+
return { ageMs, ttlMs: data.ttlSeconds * 1000 };
|
|
1372
|
+
} catch {
|
|
1373
|
+
return { ageMs: null, ttlMs: defaultTtlMs };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1292
1376
|
function writeProviderDetectionCache(baseUrl, entry) {
|
|
1293
1377
|
const path = getProviderDetectionCachePath(baseUrl);
|
|
1294
1378
|
try {
|
|
@@ -1302,27 +1386,7 @@ function writeProviderDetectionCache(baseUrl, entry) {
|
|
|
1302
1386
|
|
|
1303
1387
|
// src/providers/autodetect.ts
|
|
1304
1388
|
var detectionCache = new Map;
|
|
1305
|
-
function
|
|
1306
|
-
const includeBuiltInPatterns = options.includeBuiltInPatterns ?? true;
|
|
1307
|
-
const fallbackProvider = Object.prototype.hasOwnProperty.call(options, "fallbackProvider") ? options.fallbackProvider ?? null : "sub2api";
|
|
1308
|
-
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1309
|
-
for (const [providerId, config] of Object.entries(endpointConfigs)) {
|
|
1310
|
-
const urlPatterns = config.detection?.urlPatterns;
|
|
1311
|
-
if (urlPatterns && urlPatterns.length > 0) {
|
|
1312
|
-
for (const pattern of urlPatterns) {
|
|
1313
|
-
const normalizedPattern = pattern.toLowerCase();
|
|
1314
|
-
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1315
|
-
return providerId;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
if (includeBuiltInPatterns && (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats"))) {
|
|
1321
|
-
return "claude-relay-service";
|
|
1322
|
-
}
|
|
1323
|
-
return fallbackProvider;
|
|
1324
|
-
}
|
|
1325
|
-
async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {}, probeTimeoutMs = 1500) {
|
|
1389
|
+
async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {}, probeTimeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1326
1390
|
if (providerOverride) {
|
|
1327
1391
|
logger.debug("Provider override detected", { provider: providerOverride });
|
|
1328
1392
|
return providerOverride;
|
|
@@ -1344,33 +1408,18 @@ async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {},
|
|
|
1344
1408
|
});
|
|
1345
1409
|
return diskCached.provider;
|
|
1346
1410
|
}
|
|
1347
|
-
const endpointPatternProvider = detectProviderFromUrlPattern(baseUrl, endpointConfigs, {
|
|
1348
|
-
includeBuiltInPatterns: false,
|
|
1349
|
-
fallbackProvider: null
|
|
1350
|
-
});
|
|
1351
|
-
if (endpointPatternProvider) {
|
|
1352
|
-
logger.debug("Provider detected via endpoint URL pattern", { provider: endpointPatternProvider });
|
|
1353
|
-
cacheProviderDetection(baseUrl, endpointPatternProvider, "url-pattern");
|
|
1354
|
-
return endpointPatternProvider;
|
|
1355
|
-
}
|
|
1356
1411
|
logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
|
|
1357
|
-
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
|
|
1412
|
+
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs, endpointConfigs);
|
|
1358
1413
|
if (probedProvider) {
|
|
1359
1414
|
logger.debug("Provider detected via health probe", { provider: probedProvider });
|
|
1360
1415
|
cacheProviderDetection(baseUrl, probedProvider, "health-probe");
|
|
1361
1416
|
return probedProvider;
|
|
1362
1417
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
cacheProviderDetection(baseUrl, "sub2api", "url-pattern");
|
|
1367
|
-
return "sub2api";
|
|
1368
|
-
}
|
|
1369
|
-
logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
|
|
1370
|
-
cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
|
|
1371
|
-
return patternProvider;
|
|
1418
|
+
logger.debug("Health probe failed, defaulting to sub2api");
|
|
1419
|
+
cacheProviderDetection(baseUrl, "sub2api", "health-probe");
|
|
1420
|
+
return "sub2api";
|
|
1372
1421
|
}
|
|
1373
|
-
function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
1422
|
+
function cacheProviderDetection(baseUrl, provider, detectedVia, ttlSeconds = DETECTION_TTL_BASE_S) {
|
|
1374
1423
|
const now = new Date().toISOString();
|
|
1375
1424
|
detectionCache.set(baseUrl, {
|
|
1376
1425
|
provider,
|
|
@@ -1381,9 +1430,15 @@ function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
|
1381
1430
|
provider,
|
|
1382
1431
|
detectedVia,
|
|
1383
1432
|
detectedAt: now,
|
|
1384
|
-
ttlSeconds
|
|
1433
|
+
ttlSeconds
|
|
1385
1434
|
});
|
|
1386
1435
|
}
|
|
1436
|
+
function cacheProviderDetectionWithTtl(baseUrl, provider, ttlSeconds) {
|
|
1437
|
+
cacheProviderDetection(baseUrl, provider, "health-probe", ttlSeconds);
|
|
1438
|
+
}
|
|
1439
|
+
function invalidateDetectionCache(baseUrl) {
|
|
1440
|
+
detectionCache.delete(baseUrl);
|
|
1441
|
+
}
|
|
1387
1442
|
function clearDetectionCache() {
|
|
1388
1443
|
detectionCache.clear();
|
|
1389
1444
|
}
|
|
@@ -1539,7 +1594,7 @@ function mapPeriodTokens(data) {
|
|
|
1539
1594
|
cost: data.cost ?? 0
|
|
1540
1595
|
};
|
|
1541
1596
|
}
|
|
1542
|
-
async function fetchSub2api(baseUrl, token, config, timeoutMs =
|
|
1597
|
+
async function fetchSub2api(baseUrl, token, config, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1543
1598
|
const url = `${baseUrl}/v1/usage`;
|
|
1544
1599
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
1545
1600
|
if (resolvedUA) {
|
|
@@ -1620,7 +1675,7 @@ function computeWeeklyResetTime(resetDay, resetHour) {
|
|
|
1620
1675
|
const resetDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilReset, resetHour, 0, 0, 0));
|
|
1621
1676
|
return resetDate.toISOString();
|
|
1622
1677
|
}
|
|
1623
|
-
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs =
|
|
1678
|
+
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1624
1679
|
const origin = extractOrigin(baseUrl);
|
|
1625
1680
|
const url = `${origin}/apiStats/api/user-stats`;
|
|
1626
1681
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
@@ -1860,7 +1915,7 @@ function validateEndpointConfigSemantics(config) {
|
|
|
1860
1915
|
}
|
|
1861
1916
|
return null;
|
|
1862
1917
|
}
|
|
1863
|
-
async function fetchEndpoint(baseUrl, token, appConfig, endpointConfig, timeoutMs =
|
|
1918
|
+
async function fetchEndpoint(baseUrl, token, appConfig, endpointConfig, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1864
1919
|
const validationError = validateEndpointConfigSemantics(endpointConfig);
|
|
1865
1920
|
if (validationError) {
|
|
1866
1921
|
throw new Error(`Invalid endpoint config: ${validationError}`);
|
|
@@ -2399,11 +2454,14 @@ var TEXT_PROGRESS_ICONS = [
|
|
|
2399
2454
|
"◕",
|
|
2400
2455
|
"●"
|
|
2401
2456
|
];
|
|
2457
|
+
function calcIconIndex(percent, bucketSize, maxIndex) {
|
|
2458
|
+
return Math.min(maxIndex, Math.ceil(Math.round(percent) / bucketSize));
|
|
2459
|
+
}
|
|
2402
2460
|
function calcNerdIconIndex(percent) {
|
|
2403
|
-
return
|
|
2461
|
+
return calcIconIndex(percent, 12.5, 8);
|
|
2404
2462
|
}
|
|
2405
2463
|
function calcTextIconIndex(percent) {
|
|
2406
|
-
return
|
|
2464
|
+
return calcIconIndex(percent, 25, 4);
|
|
2407
2465
|
}
|
|
2408
2466
|
function getProgressIcon(percent, nerdFontAvailable = true) {
|
|
2409
2467
|
if (!nerdFontAvailable) {
|
|
@@ -2511,7 +2569,7 @@ function renderBalanceComponent(balance, options, componentConfig, globalConfig,
|
|
|
2511
2569
|
const isUnlimited = balance.remaining === -1;
|
|
2512
2570
|
let usagePercent = null;
|
|
2513
2571
|
if (!isUnlimited && balance.initial !== null && balance.initial > 0) {
|
|
2514
|
-
usagePercent = (balance.initial - balance.remaining
|
|
2572
|
+
usagePercent = calculateUsagePercent(balance.initial - balance.remaining, balance.initial);
|
|
2515
2573
|
}
|
|
2516
2574
|
const effectivePercent = isUnlimited ? 0 : usagePercent;
|
|
2517
2575
|
const label = renderLabel("balance", displayMode, componentConfig);
|
|
@@ -2931,6 +2989,35 @@ function isComponentId(key) {
|
|
|
2931
2989
|
return DEFAULT_COMPONENT_ORDER.includes(key);
|
|
2932
2990
|
}
|
|
2933
2991
|
|
|
2992
|
+
// src/core/error-classifier.ts
|
|
2993
|
+
function classifyFetchError(error) {
|
|
2994
|
+
if (error && typeof error === "object") {
|
|
2995
|
+
if ("statusCode" in error) {
|
|
2996
|
+
const statusCode = error.statusCode;
|
|
2997
|
+
if (statusCode === 404 || statusCode === 410) {
|
|
2998
|
+
return "site-closed";
|
|
2999
|
+
}
|
|
3000
|
+
return "transient";
|
|
3001
|
+
}
|
|
3002
|
+
if (error instanceof Error) {
|
|
3003
|
+
if (error.name === "TimeoutError") {
|
|
3004
|
+
return "transient";
|
|
3005
|
+
}
|
|
3006
|
+
if (error.name === "ResponseTooLargeError") {
|
|
3007
|
+
return "provider-mismatch";
|
|
3008
|
+
}
|
|
3009
|
+
if (error instanceof SyntaxError) {
|
|
3010
|
+
return "provider-mismatch";
|
|
3011
|
+
}
|
|
3012
|
+
const msg = error.message.toLowerCase();
|
|
3013
|
+
if (msg.includes("invalid response") || msg.includes("expected object") || msg.includes("missing data") || msg.includes("missing limits")) {
|
|
3014
|
+
return "provider-mismatch";
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
return "transient";
|
|
3019
|
+
}
|
|
3020
|
+
|
|
2934
3021
|
// src/core/execute-cycle.ts
|
|
2935
3022
|
async function executeCycle(ctx) {
|
|
2936
3023
|
const { env, config, configHash, endpointConfigHash, endpointLock, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
@@ -2943,7 +3030,9 @@ async function executeCycle(ctx) {
|
|
|
2943
3030
|
return {
|
|
2944
3031
|
output: cachedEntry.renderedLine,
|
|
2945
3032
|
exitCode: 0,
|
|
2946
|
-
cacheUpdate: null
|
|
3033
|
+
cacheUpdate: null,
|
|
3034
|
+
invalidateProvider: false,
|
|
3035
|
+
path: "A"
|
|
2947
3036
|
};
|
|
2948
3037
|
}
|
|
2949
3038
|
}
|
|
@@ -2958,14 +3047,18 @@ async function executeCycle(ctx) {
|
|
|
2958
3047
|
return {
|
|
2959
3048
|
output: statusline,
|
|
2960
3049
|
exitCode: 0,
|
|
2961
|
-
cacheUpdate: null
|
|
3050
|
+
cacheUpdate: null,
|
|
3051
|
+
invalidateProvider: false,
|
|
3052
|
+
path: "B2"
|
|
2962
3053
|
};
|
|
2963
3054
|
}
|
|
2964
3055
|
const errorOutput = renderError("endpoint-config-changed", "without-cache");
|
|
2965
3056
|
return {
|
|
2966
3057
|
output: errorOutput,
|
|
2967
3058
|
exitCode: 0,
|
|
2968
|
-
cacheUpdate: null
|
|
3059
|
+
cacheUpdate: null,
|
|
3060
|
+
invalidateProvider: false,
|
|
3061
|
+
path: "B2"
|
|
2969
3062
|
};
|
|
2970
3063
|
}
|
|
2971
3064
|
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
@@ -2980,7 +3073,9 @@ async function executeCycle(ctx) {
|
|
|
2980
3073
|
return {
|
|
2981
3074
|
output: statusline,
|
|
2982
3075
|
exitCode: 0,
|
|
2983
|
-
cacheUpdate: updatedEntry
|
|
3076
|
+
cacheUpdate: updatedEntry,
|
|
3077
|
+
invalidateProvider: false,
|
|
3078
|
+
path: "B"
|
|
2984
3079
|
};
|
|
2985
3080
|
}
|
|
2986
3081
|
const deadline = startTime + timeoutBudgetMs - EXIT_BUFFER_MS;
|
|
@@ -2991,7 +3086,9 @@ async function executeCycle(ctx) {
|
|
|
2991
3086
|
return {
|
|
2992
3087
|
output: errorOutput,
|
|
2993
3088
|
exitCode: 0,
|
|
2994
|
-
cacheUpdate: null
|
|
3089
|
+
cacheUpdate: null,
|
|
3090
|
+
invalidateProvider: false,
|
|
3091
|
+
path: "D"
|
|
2995
3092
|
};
|
|
2996
3093
|
}
|
|
2997
3094
|
try {
|
|
@@ -3001,7 +3098,9 @@ async function executeCycle(ctx) {
|
|
|
3001
3098
|
return {
|
|
3002
3099
|
output: renderError("missing-env", "without-cache"),
|
|
3003
3100
|
exitCode: 0,
|
|
3004
|
-
cacheUpdate: null
|
|
3101
|
+
cacheUpdate: null,
|
|
3102
|
+
invalidateProvider: false,
|
|
3103
|
+
path: "D"
|
|
3005
3104
|
};
|
|
3006
3105
|
}
|
|
3007
3106
|
logger.debug("Path C: Fetching from provider", { providerId, fetchTimeoutMs });
|
|
@@ -3027,23 +3126,30 @@ async function executeCycle(ctx) {
|
|
|
3027
3126
|
return {
|
|
3028
3127
|
output: statusline,
|
|
3029
3128
|
exitCode: 0,
|
|
3030
|
-
cacheUpdate: newEntry
|
|
3129
|
+
cacheUpdate: newEntry,
|
|
3130
|
+
invalidateProvider: false,
|
|
3131
|
+
path: "C"
|
|
3031
3132
|
};
|
|
3032
3133
|
} catch (error) {
|
|
3033
3134
|
logger.error("Path D: Fetch error", { error: String(error), hasCachedEntry: !!cachedEntry });
|
|
3135
|
+
const errorCategory = classifyFetchError(error);
|
|
3034
3136
|
let errorState = "network-error";
|
|
3035
3137
|
if (error && typeof error === "object" && "statusCode" in error) {
|
|
3036
3138
|
const statusCode = error.statusCode;
|
|
3037
3139
|
if (statusCode === 429) {
|
|
3038
3140
|
errorState = "rate-limited";
|
|
3141
|
+
} else if (errorCategory === "site-closed") {
|
|
3142
|
+
errorState = "network-error";
|
|
3039
3143
|
} else if (statusCode && statusCode >= 500) {
|
|
3040
3144
|
errorState = "server-error";
|
|
3041
3145
|
} else if (statusCode === 401 || statusCode === 403) {
|
|
3042
3146
|
errorState = "auth-error";
|
|
3043
3147
|
}
|
|
3148
|
+
} else if (errorCategory === "provider-mismatch") {
|
|
3149
|
+
errorState = "parse-error";
|
|
3044
3150
|
}
|
|
3045
3151
|
if (cachedEntry) {
|
|
3046
|
-
logger.debug("Discarding stale cache, showing error", { errorState });
|
|
3152
|
+
logger.debug("Discarding stale cache, showing error", { errorState, errorCategory });
|
|
3047
3153
|
} else {
|
|
3048
3154
|
logger.warn("No cache available for error fallback");
|
|
3049
3155
|
}
|
|
@@ -3051,10 +3157,32 @@ async function executeCycle(ctx) {
|
|
|
3051
3157
|
return {
|
|
3052
3158
|
output: errorOutput,
|
|
3053
3159
|
exitCode: 0,
|
|
3054
|
-
cacheUpdate: null
|
|
3160
|
+
cacheUpdate: null,
|
|
3161
|
+
invalidateProvider: errorCategory === "provider-mismatch",
|
|
3162
|
+
path: "D"
|
|
3055
3163
|
};
|
|
3056
3164
|
}
|
|
3057
3165
|
}
|
|
3166
|
+
// src/core/maintenance-scheduler.ts
|
|
3167
|
+
function selectMaintenanceTask(ctx) {
|
|
3168
|
+
if (ctx.path !== "A" && ctx.path !== "B")
|
|
3169
|
+
return "none";
|
|
3170
|
+
if (ctx.detectionCacheAgeMs === null)
|
|
3171
|
+
return "health-probe";
|
|
3172
|
+
if (ctx.detectionCacheAgeMs >= ctx.detectionCacheTtlMs * 0.5)
|
|
3173
|
+
return "health-probe";
|
|
3174
|
+
if (Math.random() < MAINTENANCE_GC_PROBABILITY)
|
|
3175
|
+
return "cache-gc";
|
|
3176
|
+
return "none";
|
|
3177
|
+
}
|
|
3178
|
+
function computeDynamicDetectionTtl(outcome, currentProvider, currentTtlSeconds) {
|
|
3179
|
+
if (!outcome.success)
|
|
3180
|
+
return DETECTION_TTL_FAILED_S;
|
|
3181
|
+
if (outcome.matchedProvider !== currentProvider)
|
|
3182
|
+
return DETECTION_TTL_CHANGED_S;
|
|
3183
|
+
return Math.min(currentTtlSeconds * 2, DETECTION_TTL_MAX_S);
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3058
3186
|
// src/services/cache-gc.ts
|
|
3059
3187
|
import { readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync6, existsSync as existsSync6 } from "fs";
|
|
3060
3188
|
import { join as join12 } from "path";
|
|
@@ -3164,6 +3292,34 @@ function runCacheGC(cacheDir) {
|
|
|
3164
3292
|
}
|
|
3165
3293
|
|
|
3166
3294
|
// src/cli/piped-mode.ts
|
|
3295
|
+
var DEFAULT_PIPED_MODE_DEPS = {
|
|
3296
|
+
readCurrentEnv,
|
|
3297
|
+
validateRequiredEnv,
|
|
3298
|
+
readCache,
|
|
3299
|
+
writeCache,
|
|
3300
|
+
getCacheDir,
|
|
3301
|
+
isCacheValid,
|
|
3302
|
+
loadConfigWithHash,
|
|
3303
|
+
loadEndpointConfigs,
|
|
3304
|
+
computeEndpointConfigHash,
|
|
3305
|
+
readEndpointLock,
|
|
3306
|
+
writeEndpointLock,
|
|
3307
|
+
needsConfigInit,
|
|
3308
|
+
writeDefaultConfigs,
|
|
3309
|
+
resolveProvider,
|
|
3310
|
+
getProvider,
|
|
3311
|
+
invalidateDetectionCache,
|
|
3312
|
+
deleteProviderDetectionCache,
|
|
3313
|
+
renderError,
|
|
3314
|
+
dimText,
|
|
3315
|
+
executeCycle,
|
|
3316
|
+
logger,
|
|
3317
|
+
runCacheGC,
|
|
3318
|
+
probeHealthWithMetrics,
|
|
3319
|
+
readDetectionCacheMeta,
|
|
3320
|
+
cacheProviderDetectionWithTtl
|
|
3321
|
+
};
|
|
3322
|
+
|
|
3167
3323
|
class StatuslineError extends Error {
|
|
3168
3324
|
errorType;
|
|
3169
3325
|
constructor(errorType) {
|
|
@@ -3176,15 +3332,15 @@ function safeStdoutWrite(data) {
|
|
|
3176
3332
|
process.stdout["write"](data);
|
|
3177
3333
|
} catch {}
|
|
3178
3334
|
}
|
|
3179
|
-
function readAndValidateEnv() {
|
|
3180
|
-
const env = readCurrentEnv();
|
|
3181
|
-
logger.debug("Environment loaded", {
|
|
3335
|
+
function readAndValidateEnv(deps) {
|
|
3336
|
+
const env = deps.readCurrentEnv();
|
|
3337
|
+
deps.logger.debug("Environment loaded", {
|
|
3182
3338
|
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
3183
3339
|
hasToken: !!env.authToken,
|
|
3184
3340
|
providerOverride: env.providerOverride,
|
|
3185
3341
|
pollIntervalOverride: env.pollIntervalOverride
|
|
3186
3342
|
});
|
|
3187
|
-
const envError = validateRequiredEnv(env);
|
|
3343
|
+
const envError = deps.validateRequiredEnv(env);
|
|
3188
3344
|
if (envError) {
|
|
3189
3345
|
throw new StatuslineError("missing-env");
|
|
3190
3346
|
}
|
|
@@ -3194,75 +3350,75 @@ function readAndValidateEnv() {
|
|
|
3194
3350
|
}
|
|
3195
3351
|
return { env, baseUrl };
|
|
3196
3352
|
}
|
|
3197
|
-
function ensureDefaultConfigs() {
|
|
3198
|
-
if (needsConfigInit()) {
|
|
3199
|
-
logger.debug("First run detected - initializing default configs");
|
|
3200
|
-
writeDefaultConfigs();
|
|
3353
|
+
function ensureDefaultConfigs(deps) {
|
|
3354
|
+
if (deps.needsConfigInit()) {
|
|
3355
|
+
deps.logger.debug("First run detected - initializing default configs");
|
|
3356
|
+
deps.writeDefaultConfigs();
|
|
3201
3357
|
}
|
|
3202
3358
|
}
|
|
3203
|
-
function loadEndpointConfigsWithHash() {
|
|
3204
|
-
const endpointConfigs = loadEndpointConfigs();
|
|
3205
|
-
const endpointConfigHash = computeEndpointConfigHash();
|
|
3206
|
-
logger.debug("Endpoint configs loaded", {
|
|
3359
|
+
function loadEndpointConfigsWithHash(deps) {
|
|
3360
|
+
const endpointConfigs = deps.loadEndpointConfigs();
|
|
3361
|
+
const endpointConfigHash = deps.computeEndpointConfigHash();
|
|
3362
|
+
deps.logger.debug("Endpoint configs loaded", {
|
|
3207
3363
|
configCount: Object.keys(endpointConfigs).length,
|
|
3208
3364
|
endpointConfigHash
|
|
3209
3365
|
});
|
|
3210
3366
|
return { endpointConfigs, endpointConfigHash };
|
|
3211
3367
|
}
|
|
3212
|
-
function resolveEndpointLock(hash) {
|
|
3213
|
-
const existing = readEndpointLock();
|
|
3368
|
+
function resolveEndpointLock(hash, deps) {
|
|
3369
|
+
const existing = deps.readEndpointLock();
|
|
3214
3370
|
if (existing) {
|
|
3215
|
-
logger.debug("Endpoint lock file loaded", {
|
|
3371
|
+
deps.logger.debug("Endpoint lock file loaded", {
|
|
3216
3372
|
lockedHash: existing.hash,
|
|
3217
3373
|
currentHash: hash,
|
|
3218
3374
|
locked: existing.hash === hash
|
|
3219
3375
|
});
|
|
3220
3376
|
return existing;
|
|
3221
3377
|
}
|
|
3222
|
-
logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3223
|
-
writeEndpointLock(hash);
|
|
3378
|
+
deps.logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3379
|
+
deps.writeEndpointLock(hash);
|
|
3224
3380
|
return { hash, lockedAt: new Date().toISOString() };
|
|
3225
3381
|
}
|
|
3226
|
-
async function resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, timeoutMs) {
|
|
3227
|
-
const probeTimeout = isPiped ? Math.
|
|
3228
|
-
const providerId = await resolveProvider(baseUrl, env.providerOverride, endpointConfigs, probeTimeout);
|
|
3229
|
-
const provider = getProvider(providerId, endpointConfigs);
|
|
3230
|
-
logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
3382
|
+
async function resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, timeoutMs, deps) {
|
|
3383
|
+
const probeTimeout = isPiped ? Math.floor(timeoutMs / 2) : timeoutMs;
|
|
3384
|
+
const providerId = await deps.resolveProvider(baseUrl, env.providerOverride, endpointConfigs, probeTimeout);
|
|
3385
|
+
const provider = deps.getProvider(providerId, endpointConfigs);
|
|
3386
|
+
deps.logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
3231
3387
|
if (!provider) {
|
|
3232
|
-
logger.error("Provider not found", { providerId });
|
|
3388
|
+
deps.logger.error("Provider not found", { providerId });
|
|
3233
3389
|
throw new StatuslineError("provider-unknown");
|
|
3234
3390
|
}
|
|
3235
3391
|
return { providerId, provider };
|
|
3236
3392
|
}
|
|
3237
3393
|
function computeTimeoutBudgets(isPiped, config, timeoutMs) {
|
|
3238
|
-
const timeoutBudgetMs = isPiped ? timeoutMs :
|
|
3239
|
-
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ??
|
|
3394
|
+
const timeoutBudgetMs = isPiped ? timeoutMs : TTY_TIMEOUT_BUDGET_MS;
|
|
3395
|
+
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? DEFAULT_TIMEOUT_BUDGET_MS, timeoutBudgetMs - TIMEOUT_HEADROOM_MS) : TTY_TIMEOUT_BUDGET_MS;
|
|
3240
3396
|
return { timeoutBudgetMs, fetchTimeoutMs };
|
|
3241
3397
|
}
|
|
3242
|
-
async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs) {
|
|
3243
|
-
const { env, baseUrl } = readAndValidateEnv();
|
|
3244
|
-
ensureDefaultConfigs();
|
|
3245
|
-
const { config, configHash } = loadConfigWithHash(args.configPath);
|
|
3246
|
-
const { endpointConfigs, endpointConfigHash } = loadEndpointConfigsWithHash();
|
|
3247
|
-
const endpointLock = resolveEndpointLock(endpointConfigHash);
|
|
3248
|
-
const cachedEntry = readCache(baseUrl);
|
|
3249
|
-
logger.debug("Cache read", {
|
|
3398
|
+
async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs, deps) {
|
|
3399
|
+
const { env, baseUrl } = readAndValidateEnv(deps);
|
|
3400
|
+
ensureDefaultConfigs(deps);
|
|
3401
|
+
const { config, configHash } = deps.loadConfigWithHash(args.configPath);
|
|
3402
|
+
const { endpointConfigs, endpointConfigHash } = loadEndpointConfigsWithHash(deps);
|
|
3403
|
+
const endpointLock = resolveEndpointLock(endpointConfigHash, deps);
|
|
3404
|
+
const cachedEntry = deps.readCache(baseUrl);
|
|
3405
|
+
deps.logger.debug("Cache read", {
|
|
3250
3406
|
cacheHit: !!cachedEntry,
|
|
3251
3407
|
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
3252
3408
|
});
|
|
3253
3409
|
let providerId;
|
|
3254
3410
|
let provider;
|
|
3255
|
-
if (cachedEntry && isCacheValid(cachedEntry, env)) {
|
|
3256
|
-
const cachedProvider = getProvider(cachedEntry.provider, endpointConfigs);
|
|
3411
|
+
if (cachedEntry && deps.isCacheValid(cachedEntry, env)) {
|
|
3412
|
+
const cachedProvider = deps.getProvider(cachedEntry.provider, endpointConfigs);
|
|
3257
3413
|
if (cachedProvider) {
|
|
3258
3414
|
providerId = cachedEntry.provider;
|
|
3259
3415
|
provider = cachedProvider;
|
|
3260
|
-
logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3416
|
+
deps.logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3261
3417
|
} else {
|
|
3262
|
-
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3418
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs, deps));
|
|
3263
3419
|
}
|
|
3264
3420
|
} else {
|
|
3265
|
-
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3421
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs, deps));
|
|
3266
3422
|
}
|
|
3267
3423
|
const { timeoutBudgetMs, fetchTimeoutMs } = computeTimeoutBudgets(isPiped, config, rawTimeoutMs);
|
|
3268
3424
|
const ctx = {
|
|
@@ -3278,82 +3434,124 @@ async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs) {
|
|
|
3278
3434
|
startTime,
|
|
3279
3435
|
fetchTimeoutMs
|
|
3280
3436
|
};
|
|
3281
|
-
return { ctx, baseUrl };
|
|
3437
|
+
return { ctx, baseUrl, endpointConfigs };
|
|
3282
3438
|
}
|
|
3283
|
-
function formatOutput(output,
|
|
3439
|
+
function formatOutput(output, mode, log) {
|
|
3284
3440
|
let normalizedOutput = output;
|
|
3285
3441
|
if (!normalizedOutput || normalizedOutput.trim().length === 0) {
|
|
3286
|
-
|
|
3442
|
+
log.debug("Empty output detected, using fallback");
|
|
3287
3443
|
normalizedOutput = "[loading...]";
|
|
3288
3444
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3445
|
+
switch (mode) {
|
|
3446
|
+
case "piped-embedded":
|
|
3447
|
+
log.debug("Output written (embedded piped mode - no host formatting)");
|
|
3448
|
+
return normalizedOutput;
|
|
3449
|
+
case "piped":
|
|
3450
|
+
log.debug("Output formatted for piped mode (ANSI reset + NBSP)");
|
|
3451
|
+
return "\x1B[0m" + normalizedOutput.replace(/ /g, " ");
|
|
3452
|
+
case "tty":
|
|
3453
|
+
log.debug("Output written (TTY mode)");
|
|
3454
|
+
return normalizedOutput + `
|
|
3455
|
+
`;
|
|
3295
3456
|
}
|
|
3296
3457
|
}
|
|
3297
|
-
async function
|
|
3458
|
+
async function runMaintenance(result, baseUrl, startTime, budgetMs, endpointConfigs, currentProviderId, deps) {
|
|
3459
|
+
const { ageMs, ttlMs } = deps.readDetectionCacheMeta(baseUrl);
|
|
3460
|
+
const task = selectMaintenanceTask({
|
|
3461
|
+
path: result.path,
|
|
3462
|
+
detectionCacheAgeMs: ageMs,
|
|
3463
|
+
detectionCacheTtlMs: ttlMs
|
|
3464
|
+
});
|
|
3465
|
+
if (task === "none")
|
|
3466
|
+
return;
|
|
3467
|
+
deps.logger.debug("Maintenance task selected", { task, path: result.path });
|
|
3468
|
+
if (task === "health-probe") {
|
|
3469
|
+
const elapsed = Date.now() - startTime;
|
|
3470
|
+
const remainingMs = Math.max(50, budgetMs - elapsed - TIMEOUT_HEADROOM_MS);
|
|
3471
|
+
const currentTtlS = Math.floor(ttlMs / 1000) || DETECTION_TTL_BASE_S;
|
|
3472
|
+
const outcome = await deps.probeHealthWithMetrics(baseUrl, remainingMs, endpointConfigs);
|
|
3473
|
+
deps.logger.debug("Maintenance probe completed", {
|
|
3474
|
+
success: outcome.success,
|
|
3475
|
+
matchedProvider: outcome.matchedProvider,
|
|
3476
|
+
responseTimeMs: outcome.responseTimeMs
|
|
3477
|
+
});
|
|
3478
|
+
if (outcome.success && outcome.matchedProvider) {
|
|
3479
|
+
const newTtlS = computeDynamicDetectionTtl(outcome, currentProviderId, currentTtlS);
|
|
3480
|
+
deps.cacheProviderDetectionWithTtl(baseUrl, outcome.matchedProvider, newTtlS);
|
|
3481
|
+
deps.logger.debug("Detection cache refreshed", { ttlSeconds: newTtlS, provider: outcome.matchedProvider });
|
|
3482
|
+
}
|
|
3483
|
+
} else if (task === "cache-gc") {
|
|
3484
|
+
deps.runCacheGC(deps.getCacheDir());
|
|
3485
|
+
deps.logger.debug("Cache GC completed");
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
async function executePipedMode(args, deps = DEFAULT_PIPED_MODE_DEPS) {
|
|
3298
3489
|
const startTime = Date.now();
|
|
3299
|
-
logger.debug("=== cc-api-statusline execution started ===");
|
|
3300
|
-
logger.debug("Start time", { startTime });
|
|
3490
|
+
deps.logger.debug("=== cc-api-statusline execution started ===");
|
|
3491
|
+
deps.logger.debug("Start time", { startTime });
|
|
3301
3492
|
const isPiped = !process.stdin.isTTY;
|
|
3302
|
-
|
|
3303
|
-
|
|
3493
|
+
const outputMode = !isPiped ? "tty" : args.embedded ? "piped-embedded" : "piped";
|
|
3494
|
+
deps.logger.debug("Mode detection", { isPiped, once: args.once, outputMode });
|
|
3495
|
+
const rawTimeoutMs = Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? DEFAULT_TIMEOUT_BUDGET_MS);
|
|
3304
3496
|
if (isPiped) {
|
|
3305
|
-
const watchdogMs = rawTimeoutMs -
|
|
3497
|
+
const watchdogMs = rawTimeoutMs - TIMEOUT_HEADROOM_MS;
|
|
3306
3498
|
setTimeout(() => {
|
|
3307
|
-
logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
|
|
3308
|
-
const fallback = dimText("⟳ Refreshing...");
|
|
3309
|
-
const formatted = formatOutput(fallback,
|
|
3499
|
+
deps.logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
|
|
3500
|
+
const fallback = deps.dimText("⟳ Refreshing...");
|
|
3501
|
+
const formatted = formatOutput(fallback, outputMode, deps.logger);
|
|
3310
3502
|
safeStdoutWrite(formatted);
|
|
3311
3503
|
process.exit(0);
|
|
3312
3504
|
}, watchdogMs).unref();
|
|
3313
3505
|
}
|
|
3314
3506
|
let ctx;
|
|
3315
3507
|
let baseUrl;
|
|
3508
|
+
let endpointConfigs;
|
|
3316
3509
|
try {
|
|
3317
|
-
({ ctx, baseUrl } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs));
|
|
3510
|
+
({ ctx, baseUrl, endpointConfigs } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs, deps));
|
|
3318
3511
|
} catch (error) {
|
|
3319
|
-
logger.error("Failed to build execution context", { error: String(error) });
|
|
3512
|
+
deps.logger.error("Failed to build execution context", { error: String(error) });
|
|
3320
3513
|
const errorType = error instanceof StatuslineError ? error.errorType : "network-error";
|
|
3321
|
-
const errorOutput = renderError(errorType, "without-cache");
|
|
3322
|
-
const formattedOutput2 = formatOutput(errorOutput,
|
|
3514
|
+
const errorOutput = deps.renderError(errorType, "without-cache");
|
|
3515
|
+
const formattedOutput2 = formatOutput(errorOutput, outputMode, deps.logger);
|
|
3323
3516
|
safeStdoutWrite(formattedOutput2);
|
|
3324
|
-
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3517
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3325
3518
|
process.exit(0);
|
|
3326
3519
|
}
|
|
3327
|
-
logger.debug("Execution context prepared", {
|
|
3520
|
+
deps.logger.debug("Execution context prepared", {
|
|
3328
3521
|
timeoutBudgetMs: ctx.timeoutBudgetMs,
|
|
3329
3522
|
fetchTimeoutMs: ctx.fetchTimeoutMs
|
|
3330
3523
|
});
|
|
3331
3524
|
let result;
|
|
3332
3525
|
try {
|
|
3333
|
-
result = await executeCycle(ctx);
|
|
3526
|
+
result = await deps.executeCycle(ctx);
|
|
3334
3527
|
} catch (error) {
|
|
3335
|
-
logger.error("Execution cycle failed", { error: String(error) });
|
|
3336
|
-
const errorOutput = renderError("network-error", "without-cache");
|
|
3337
|
-
const formattedOutput2 = formatOutput(errorOutput,
|
|
3528
|
+
deps.logger.error("Execution cycle failed", { error: String(error) });
|
|
3529
|
+
const errorOutput = deps.renderError("network-error", "without-cache");
|
|
3530
|
+
const formattedOutput2 = formatOutput(errorOutput, outputMode, deps.logger);
|
|
3338
3531
|
safeStdoutWrite(formattedOutput2);
|
|
3339
|
-
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3532
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3340
3533
|
process.exit(0);
|
|
3341
3534
|
}
|
|
3342
3535
|
const executionTime = Date.now() - startTime;
|
|
3343
|
-
logger.debug("Execution completed", {
|
|
3536
|
+
deps.logger.debug("Execution completed", {
|
|
3344
3537
|
exitCode: result.exitCode,
|
|
3345
3538
|
executionTime: `${executionTime}ms`,
|
|
3346
3539
|
outputLength: result.output.length,
|
|
3347
3540
|
cacheUpdate: !!result.cacheUpdate
|
|
3348
3541
|
});
|
|
3349
|
-
const formattedOutput = formatOutput(result.output,
|
|
3542
|
+
const formattedOutput = formatOutput(result.output, outputMode, deps.logger);
|
|
3350
3543
|
safeStdoutWrite(formattedOutput);
|
|
3544
|
+
if (result.invalidateProvider) {
|
|
3545
|
+
deps.invalidateDetectionCache(baseUrl);
|
|
3546
|
+
deps.deleteProviderDetectionCache(baseUrl);
|
|
3547
|
+
deps.logger.debug("Provider detection cache invalidated", { baseUrl });
|
|
3548
|
+
}
|
|
3351
3549
|
if (result.cacheUpdate) {
|
|
3352
|
-
writeCache(baseUrl, result.cacheUpdate);
|
|
3353
|
-
logger.debug("Cache written", { baseUrl });
|
|
3354
|
-
runCacheGC(getCacheDir());
|
|
3550
|
+
deps.writeCache(baseUrl, result.cacheUpdate);
|
|
3551
|
+
deps.logger.debug("Cache written", { baseUrl });
|
|
3355
3552
|
}
|
|
3356
|
-
|
|
3553
|
+
await runMaintenance(result, baseUrl, startTime, rawTimeoutMs, endpointConfigs, ctx.providerId, deps);
|
|
3554
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3357
3555
|
process.exit(result.exitCode);
|
|
3358
3556
|
}
|
|
3359
3557
|
// src/main.ts
|