cc-api-statusline 1.0.2 → 1.1.2
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 +28 -30
- package/README.zh-CN.md +23 -28
- package/dist/cc-api-statusline.js +352 -153
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
6
8
|
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
|
-
- ⚡ **Fast piped mode** — <25ms warm cache, <100ms p95
|
|
10
11
|
- 🎨 **Highly configurable** — Layouts, colors, bar styles, display modes
|
|
11
12
|
- 🔌 **Provider autodetection** — Works with sub2api, claude-relay-service, custom providers
|
|
12
|
-
- 💾 **Smart caching** — Disk cache with atomic writes, TTL validation, automatic garbage collection
|
|
13
|
-
- 🎯 **Claude Code integration** — Auto-setup with `--install` command
|
|
14
13
|
- 📊 **Multiple components** — Daily/weekly/monthly quotas, balance, tokens, rate limits
|
|
15
14
|
- 🔁 **Hot switching** — Auto-detects API endpoint and credential changes at runtime
|
|
16
15
|
- 🔒 **Reliability** — No stale data display, race-condition-free writes, auto cache cleanup
|
|
@@ -45,7 +44,7 @@ export ANTHROPIC_AUTH_TOKEN="your-api-token"
|
|
|
45
44
|
bunx cc-api-statusline@latest --once
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
### 3. Install as Claude Code widget
|
|
47
|
+
### 3.a Install as Claude Code widget
|
|
49
48
|
|
|
50
49
|
```bash
|
|
51
50
|
bunx cc-api-statusline@latest --install
|
|
@@ -56,6 +55,7 @@ This adds to `~/.claude/settings.json`:
|
|
|
56
55
|
```json
|
|
57
56
|
{
|
|
58
57
|
"statusLine": {
|
|
58
|
+
"id": "e62435aa-bdf5-4ded-a2e3-ae13582439db",
|
|
59
59
|
"type": "command",
|
|
60
60
|
"command": "bunx -y cc-api-statusline@latest",
|
|
61
61
|
"padding": 0
|
|
@@ -63,6 +63,28 @@ This adds to `~/.claude/settings.json`:
|
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
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
|
+
|
|
66
88
|
Using `bunx` ensures you always run the latest version without a global install. To uninstall:
|
|
67
89
|
|
|
68
90
|
```bash
|
|
@@ -144,31 +166,6 @@ cc-api-statusline --apply-config
|
|
|
144
166
|
|
|
145
167
|
See [docs/api-config-reference.md](docs/api-config-reference.md) for the full schema.
|
|
146
168
|
|
|
147
|
-
## [ccstatusline](https://github.com/anthropics/claude-code) Custom Command
|
|
148
|
-
|
|
149
|
-
Add to `~/.claude/ccstatusline/config.json`:
|
|
150
|
-
|
|
151
|
-
```json
|
|
152
|
-
{
|
|
153
|
-
"customCommands": {
|
|
154
|
-
"usage": {
|
|
155
|
-
"command": "cc-api-statusline",
|
|
156
|
-
"description": "API usage statusline",
|
|
157
|
-
"type": "piped"
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
"widgets": [
|
|
161
|
-
{
|
|
162
|
-
"type": "customCommand",
|
|
163
|
-
"command": "usage",
|
|
164
|
-
"refreshIntervalMs": 30000,
|
|
165
|
-
"maxWidth": 100,
|
|
166
|
-
"preserveColors": true
|
|
167
|
-
}
|
|
168
|
-
]
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
169
|
## Environment Variables
|
|
173
170
|
|
|
174
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)).
|
|
@@ -180,6 +177,7 @@ All variables are optional at the shell level — `ANTHROPIC_BASE_URL` and `ANTH
|
|
|
180
177
|
| `CC_STATUSLINE_PROVIDER` | Yes | Override provider detection (`sub2api`, `claude-relay-service`, or custom) |
|
|
181
178
|
| `CC_STATUSLINE_POLL` | Yes | Override poll interval (seconds, min 5) |
|
|
182
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 |
|
|
183
181
|
| `DEBUG` or `CC_STATUSLINE_DEBUG` | Yes | Enable debug logging to `~/.claude/cc-api-statusline/debug.log` |
|
|
184
182
|
|
|
185
183
|
## Troubleshooting
|
package/README.zh-CN.md
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
在ClaudeCode状态栏显示API用量,通过轮询 Claude API
|
|
5
|
+
在ClaudeCode状态栏显示API用量,通过轮询 Claude API 服务(sub2api、claude-relay-service 或自定义提供商)获取用量数据,并以可配置显示样式。
|
|
6
6
|
|
|
7
7
|
## 特性
|
|
8
8
|
|
|
9
9
|
- 🎨 **高度可配置** — 布局、颜色、进度条样式、显示模式任意调整
|
|
10
10
|
- 🔌 **提供商自动识别** — 开箱支持 sub2api、claude-relay-service 及自定义提供商
|
|
11
|
-
- 💾 **智能缓存** — 原子写入磁盘缓存、TTL 验证、自动垃圾回收
|
|
12
11
|
- 🎯 **Claude Code 集成** — 一键 `--install` 完成安装
|
|
13
12
|
- 📊 **多维度用量展示** — 每日/每周/每月配额、余额、Token数、速率限制
|
|
14
13
|
- 🔁 **热切换** — 自动感知 API 端点和凭证变更,无需重启
|
|
@@ -44,7 +43,7 @@ export ANTHROPIC_AUTH_TOKEN="your-api-token"
|
|
|
44
43
|
bunx cc-api-statusline@latest --once
|
|
45
44
|
```
|
|
46
45
|
|
|
47
|
-
### 3. 安装为 Claude Code
|
|
46
|
+
### 3.a 安装为 Claude Code 状态栏组件
|
|
48
47
|
|
|
49
48
|
```bash
|
|
50
49
|
bunx cc-api-statusline@latest --install
|
|
@@ -61,6 +60,27 @@ bunx cc-api-statusline@latest --install
|
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
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 当前运行在宿主渲染器内部,由宿主负责格式化。
|
|
64
84
|
|
|
65
85
|
使用 `bunx` 可每次自动拉取最新版本,无需全局安装。如需卸载:
|
|
66
86
|
|
|
@@ -143,31 +163,6 @@ cc-api-statusline --apply-config
|
|
|
143
163
|
|
|
144
164
|
完整 Schema 请参阅 [docs/api-config-reference.md](docs/api-config-reference.md)。
|
|
145
165
|
|
|
146
|
-
## [ccstatusline](https://github.com/anthropics/claude-code) 自定义命令
|
|
147
|
-
|
|
148
|
-
在 `~/.claude/ccstatusline/config.json` 中添加如下配置:
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
{
|
|
152
|
-
"customCommands": {
|
|
153
|
-
"usage": {
|
|
154
|
-
"command": "cc-api-statusline",
|
|
155
|
-
"description": "API usage statusline",
|
|
156
|
-
"type": "piped"
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
"widgets": [
|
|
160
|
-
{
|
|
161
|
-
"type": "customCommand",
|
|
162
|
-
"command": "usage",
|
|
163
|
-
"refreshIntervalMs": 30000,
|
|
164
|
-
"maxWidth": 100,
|
|
165
|
-
"preserveColors": true
|
|
166
|
-
}
|
|
167
|
-
]
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
166
|
## 环境变量
|
|
172
167
|
|
|
173
168
|
以下所有变量均为可选——`ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 可通过 `settings.json` 的 env 字段配置,无需在 Shell 中手动导出(详见[快速上手](#快速上手))。
|
|
@@ -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.2",
|
|
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,9 +184,10 @@ import { spawn } from "child_process";
|
|
|
177
184
|
import { dirname, join } from "path";
|
|
178
185
|
|
|
179
186
|
// src/core/constants.ts
|
|
180
|
-
var
|
|
181
|
-
var
|
|
187
|
+
var DEFAULT_TIMEOUT_BUDGET_MS = 5000;
|
|
188
|
+
var TTY_TIMEOUT_BUDGET_MS = DEFAULT_TIMEOUT_BUDGET_MS * 2;
|
|
182
189
|
var EXIT_BUFFER_MS = 50;
|
|
190
|
+
var TIMEOUT_HEADROOM_MS = 100;
|
|
183
191
|
var STALENESS_THRESHOLD_MINUTES = 5;
|
|
184
192
|
var VERY_STALE_THRESHOLD_MINUTES = 30;
|
|
185
193
|
var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -190,6 +198,12 @@ var LOG_ROTATION_PROBABILITY = 0.05;
|
|
|
190
198
|
var LOG_MAX_SIZE_BYTES = 512 * 1024;
|
|
191
199
|
var LOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
192
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;
|
|
193
207
|
|
|
194
208
|
// src/services/log-rotator.ts
|
|
195
209
|
var ARCHIVE_LOG_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log$/;
|
|
@@ -564,7 +578,7 @@ var DEFAULT_CONFIG = {
|
|
|
564
578
|
chill: { tiers: buildTiers(["cyan", "cyan", "blue", "blue", "magenta"]) }
|
|
565
579
|
},
|
|
566
580
|
pollIntervalSeconds: 30,
|
|
567
|
-
pipedRequestTimeoutMs:
|
|
581
|
+
pipedRequestTimeoutMs: DEFAULT_TIMEOUT_BUDGET_MS
|
|
568
582
|
};
|
|
569
583
|
var BAR_SIZE_MAP = {
|
|
570
584
|
small: 4,
|
|
@@ -636,12 +650,11 @@ function isCacheEntry(value) {
|
|
|
636
650
|
const c = value;
|
|
637
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");
|
|
638
652
|
}
|
|
639
|
-
var PROVIDER_DETECTION_TTL_SECONDS = 86400;
|
|
640
653
|
function isProviderDetectionCacheEntry(value) {
|
|
641
654
|
if (typeof value !== "object" || value === null)
|
|
642
655
|
return false;
|
|
643
656
|
const c = value;
|
|
644
|
-
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";
|
|
645
658
|
}
|
|
646
659
|
// src/services/endpoint-config.ts
|
|
647
660
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
@@ -814,7 +827,6 @@ function getBuiltInEndpointConfigs() {
|
|
|
814
827
|
resetSemantics: "rolling-window"
|
|
815
828
|
},
|
|
816
829
|
detection: {
|
|
817
|
-
urlPatterns: ["/apistats", "/api/user-stats"],
|
|
818
830
|
healthMatch: { service: "*" }
|
|
819
831
|
},
|
|
820
832
|
responseMapping: {
|
|
@@ -999,7 +1011,15 @@ function writeDefaultConfigs(customDir) {
|
|
|
999
1011
|
ensureDir(apiConfigDir);
|
|
1000
1012
|
if (!existsSync4(configPath)) {
|
|
1001
1013
|
const styleConfigWithoutColors = serializableConfig(getDefaultStyleConfig());
|
|
1002
|
-
|
|
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), {
|
|
1003
1023
|
appendNewline: true
|
|
1004
1024
|
});
|
|
1005
1025
|
}
|
|
@@ -1091,7 +1111,7 @@ async function readBodyWithLimit(response) {
|
|
|
1091
1111
|
throw new HttpError(`Failed to read response body: ${error}`);
|
|
1092
1112
|
}
|
|
1093
1113
|
}
|
|
1094
|
-
async function secureFetch(url, options = {}, timeoutMs =
|
|
1114
|
+
async function secureFetch(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS, userAgent) {
|
|
1095
1115
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
1096
1116
|
const fetchOptions = {
|
|
1097
1117
|
...options,
|
|
@@ -1140,7 +1160,33 @@ function extractOrigin(baseUrl) {
|
|
|
1140
1160
|
return baseUrl;
|
|
1141
1161
|
}
|
|
1142
1162
|
}
|
|
1143
|
-
|
|
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 = {}) {
|
|
1144
1190
|
const origin = extractOrigin(baseUrl);
|
|
1145
1191
|
const healthUrl = `${origin}/health`;
|
|
1146
1192
|
logger.debug("Probing health endpoint", { healthUrl, timeoutMs });
|
|
@@ -1153,13 +1199,10 @@ async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
|
1153
1199
|
}, timeoutMs);
|
|
1154
1200
|
const data = JSON.parse(responseText);
|
|
1155
1201
|
logger.debug("Health probe response", { data });
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
if (data["status"] === "ok") {
|
|
1161
|
-
logger.debug("Detected sub2api from status: ok pattern");
|
|
1162
|
-
return "sub2api";
|
|
1202
|
+
const matched = matchHealthResponse(data, endpointConfigs);
|
|
1203
|
+
if (matched) {
|
|
1204
|
+
logger.debug("Detected provider from health response", { provider: matched });
|
|
1205
|
+
return matched;
|
|
1163
1206
|
}
|
|
1164
1207
|
logger.debug("Health probe returned unrecognized pattern", { data });
|
|
1165
1208
|
return null;
|
|
@@ -1168,6 +1211,15 @@ async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
|
1168
1211
|
return null;
|
|
1169
1212
|
}
|
|
1170
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
|
+
}
|
|
1171
1223
|
|
|
1172
1224
|
// src/services/cache.ts
|
|
1173
1225
|
import { readFileSync as readFileSync6, unlinkSync as unlinkSync4 } from "fs";
|
|
@@ -1290,6 +1342,37 @@ function readProviderDetectionCache(baseUrl) {
|
|
|
1290
1342
|
return null;
|
|
1291
1343
|
}
|
|
1292
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
|
+
}
|
|
1293
1376
|
function writeProviderDetectionCache(baseUrl, entry) {
|
|
1294
1377
|
const path = getProviderDetectionCachePath(baseUrl);
|
|
1295
1378
|
try {
|
|
@@ -1303,27 +1386,7 @@ function writeProviderDetectionCache(baseUrl, entry) {
|
|
|
1303
1386
|
|
|
1304
1387
|
// src/providers/autodetect.ts
|
|
1305
1388
|
var detectionCache = new Map;
|
|
1306
|
-
function
|
|
1307
|
-
const includeBuiltInPatterns = options.includeBuiltInPatterns ?? true;
|
|
1308
|
-
const fallbackProvider = Object.prototype.hasOwnProperty.call(options, "fallbackProvider") ? options.fallbackProvider ?? null : "sub2api";
|
|
1309
|
-
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1310
|
-
for (const [providerId, config] of Object.entries(endpointConfigs)) {
|
|
1311
|
-
const urlPatterns = config.detection?.urlPatterns;
|
|
1312
|
-
if (urlPatterns && urlPatterns.length > 0) {
|
|
1313
|
-
for (const pattern of urlPatterns) {
|
|
1314
|
-
const normalizedPattern = pattern.toLowerCase();
|
|
1315
|
-
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1316
|
-
return providerId;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
if (includeBuiltInPatterns && (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats"))) {
|
|
1322
|
-
return "claude-relay-service";
|
|
1323
|
-
}
|
|
1324
|
-
return fallbackProvider;
|
|
1325
|
-
}
|
|
1326
|
-
async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {}, probeTimeoutMs = 1500) {
|
|
1389
|
+
async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {}, probeTimeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1327
1390
|
if (providerOverride) {
|
|
1328
1391
|
logger.debug("Provider override detected", { provider: providerOverride });
|
|
1329
1392
|
return providerOverride;
|
|
@@ -1345,33 +1408,18 @@ async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {},
|
|
|
1345
1408
|
});
|
|
1346
1409
|
return diskCached.provider;
|
|
1347
1410
|
}
|
|
1348
|
-
const endpointPatternProvider = detectProviderFromUrlPattern(baseUrl, endpointConfigs, {
|
|
1349
|
-
includeBuiltInPatterns: false,
|
|
1350
|
-
fallbackProvider: null
|
|
1351
|
-
});
|
|
1352
|
-
if (endpointPatternProvider) {
|
|
1353
|
-
logger.debug("Provider detected via endpoint URL pattern", { provider: endpointPatternProvider });
|
|
1354
|
-
cacheProviderDetection(baseUrl, endpointPatternProvider, "url-pattern");
|
|
1355
|
-
return endpointPatternProvider;
|
|
1356
|
-
}
|
|
1357
1411
|
logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
|
|
1358
|
-
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
|
|
1412
|
+
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs, endpointConfigs);
|
|
1359
1413
|
if (probedProvider) {
|
|
1360
1414
|
logger.debug("Provider detected via health probe", { provider: probedProvider });
|
|
1361
1415
|
cacheProviderDetection(baseUrl, probedProvider, "health-probe");
|
|
1362
1416
|
return probedProvider;
|
|
1363
1417
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
cacheProviderDetection(baseUrl, "sub2api", "url-pattern");
|
|
1368
|
-
return "sub2api";
|
|
1369
|
-
}
|
|
1370
|
-
logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
|
|
1371
|
-
cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
|
|
1372
|
-
return patternProvider;
|
|
1418
|
+
logger.debug("Health probe failed, defaulting to sub2api");
|
|
1419
|
+
cacheProviderDetection(baseUrl, "sub2api", "health-probe");
|
|
1420
|
+
return "sub2api";
|
|
1373
1421
|
}
|
|
1374
|
-
function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
1422
|
+
function cacheProviderDetection(baseUrl, provider, detectedVia, ttlSeconds = DETECTION_TTL_BASE_S) {
|
|
1375
1423
|
const now = new Date().toISOString();
|
|
1376
1424
|
detectionCache.set(baseUrl, {
|
|
1377
1425
|
provider,
|
|
@@ -1382,9 +1430,15 @@ function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
|
1382
1430
|
provider,
|
|
1383
1431
|
detectedVia,
|
|
1384
1432
|
detectedAt: now,
|
|
1385
|
-
ttlSeconds
|
|
1433
|
+
ttlSeconds
|
|
1386
1434
|
});
|
|
1387
1435
|
}
|
|
1436
|
+
function cacheProviderDetectionWithTtl(baseUrl, provider, ttlSeconds) {
|
|
1437
|
+
cacheProviderDetection(baseUrl, provider, "health-probe", ttlSeconds);
|
|
1438
|
+
}
|
|
1439
|
+
function invalidateDetectionCache(baseUrl) {
|
|
1440
|
+
detectionCache.delete(baseUrl);
|
|
1441
|
+
}
|
|
1388
1442
|
function clearDetectionCache() {
|
|
1389
1443
|
detectionCache.clear();
|
|
1390
1444
|
}
|
|
@@ -1540,7 +1594,7 @@ function mapPeriodTokens(data) {
|
|
|
1540
1594
|
cost: data.cost ?? 0
|
|
1541
1595
|
};
|
|
1542
1596
|
}
|
|
1543
|
-
async function fetchSub2api(baseUrl, token, config, timeoutMs =
|
|
1597
|
+
async function fetchSub2api(baseUrl, token, config, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1544
1598
|
const url = `${baseUrl}/v1/usage`;
|
|
1545
1599
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
1546
1600
|
if (resolvedUA) {
|
|
@@ -1621,7 +1675,7 @@ function computeWeeklyResetTime(resetDay, resetHour) {
|
|
|
1621
1675
|
const resetDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilReset, resetHour, 0, 0, 0));
|
|
1622
1676
|
return resetDate.toISOString();
|
|
1623
1677
|
}
|
|
1624
|
-
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs =
|
|
1678
|
+
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1625
1679
|
const origin = extractOrigin(baseUrl);
|
|
1626
1680
|
const url = `${origin}/apiStats/api/user-stats`;
|
|
1627
1681
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
@@ -1861,7 +1915,7 @@ function validateEndpointConfigSemantics(config) {
|
|
|
1861
1915
|
}
|
|
1862
1916
|
return null;
|
|
1863
1917
|
}
|
|
1864
|
-
async function fetchEndpoint(baseUrl, token, appConfig, endpointConfig, timeoutMs =
|
|
1918
|
+
async function fetchEndpoint(baseUrl, token, appConfig, endpointConfig, timeoutMs = DEFAULT_TIMEOUT_BUDGET_MS) {
|
|
1865
1919
|
const validationError = validateEndpointConfigSemantics(endpointConfig);
|
|
1866
1920
|
if (validationError) {
|
|
1867
1921
|
throw new Error(`Invalid endpoint config: ${validationError}`);
|
|
@@ -1951,7 +2005,7 @@ var ANSI_COLORS = {
|
|
|
1951
2005
|
grey: "\x1B[90m"
|
|
1952
2006
|
};
|
|
1953
2007
|
var THEME_COLORS = {
|
|
1954
|
-
cool: "#
|
|
2008
|
+
cool: "#569AD4",
|
|
1955
2009
|
comfortable: "#5EBE8A",
|
|
1956
2010
|
warm: "#C9A84C",
|
|
1957
2011
|
hot: "#D68B45",
|
|
@@ -2428,6 +2482,10 @@ function getProgressIcon(percent, nerdFontAvailable = true) {
|
|
|
2428
2482
|
|
|
2429
2483
|
// src/renderer/format.ts
|
|
2430
2484
|
function formatCurrency(n) {
|
|
2485
|
+
if (n > 0 && n < 10) {
|
|
2486
|
+
const rounded = Math.round(n * 10) / 10;
|
|
2487
|
+
return `$${rounded.toFixed(1)}`;
|
|
2488
|
+
}
|
|
2431
2489
|
return `$${Math.floor(n)}`;
|
|
2432
2490
|
}
|
|
2433
2491
|
function formatCurrencyQuota(used, limit) {
|
|
@@ -2504,7 +2562,8 @@ function renderQuotaComponent(componentId, quota, options, componentConfig, glob
|
|
|
2504
2562
|
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
2505
2563
|
const countdownColor = resolvePartColor("countdown", usagePercent, componentConfig, globalConfig);
|
|
2506
2564
|
const progress = renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, null, renderContext);
|
|
2507
|
-
const
|
|
2565
|
+
const percentText = usagePercent > 0 && usagePercent < 10 ? `${(Math.round(usagePercent * 10) / 10).toFixed(1)}%` : `${Math.round(usagePercent)}%`;
|
|
2566
|
+
const value = showPercentage ? ansiColor(percentText, valueColor, renderContext) : "";
|
|
2508
2567
|
const countdown = renderSecondaryDisplay(quota.resetsAt, quota, componentConfig.countdown, countdownColor, clockFormat, renderContext);
|
|
2509
2568
|
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
2510
2569
|
}
|
|
@@ -2935,6 +2994,35 @@ function isComponentId(key) {
|
|
|
2935
2994
|
return DEFAULT_COMPONENT_ORDER.includes(key);
|
|
2936
2995
|
}
|
|
2937
2996
|
|
|
2997
|
+
// src/core/error-classifier.ts
|
|
2998
|
+
function classifyFetchError(error) {
|
|
2999
|
+
if (error && typeof error === "object") {
|
|
3000
|
+
if ("statusCode" in error) {
|
|
3001
|
+
const statusCode = error.statusCode;
|
|
3002
|
+
if (statusCode === 404 || statusCode === 410) {
|
|
3003
|
+
return "site-closed";
|
|
3004
|
+
}
|
|
3005
|
+
return "transient";
|
|
3006
|
+
}
|
|
3007
|
+
if (error instanceof Error) {
|
|
3008
|
+
if (error.name === "TimeoutError") {
|
|
3009
|
+
return "transient";
|
|
3010
|
+
}
|
|
3011
|
+
if (error.name === "ResponseTooLargeError") {
|
|
3012
|
+
return "provider-mismatch";
|
|
3013
|
+
}
|
|
3014
|
+
if (error instanceof SyntaxError) {
|
|
3015
|
+
return "provider-mismatch";
|
|
3016
|
+
}
|
|
3017
|
+
const msg = error.message.toLowerCase();
|
|
3018
|
+
if (msg.includes("invalid response") || msg.includes("expected object") || msg.includes("missing data") || msg.includes("missing limits")) {
|
|
3019
|
+
return "provider-mismatch";
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
return "transient";
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2938
3026
|
// src/core/execute-cycle.ts
|
|
2939
3027
|
async function executeCycle(ctx) {
|
|
2940
3028
|
const { env, config, configHash, endpointConfigHash, endpointLock, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
@@ -2947,7 +3035,9 @@ async function executeCycle(ctx) {
|
|
|
2947
3035
|
return {
|
|
2948
3036
|
output: cachedEntry.renderedLine,
|
|
2949
3037
|
exitCode: 0,
|
|
2950
|
-
cacheUpdate: null
|
|
3038
|
+
cacheUpdate: null,
|
|
3039
|
+
invalidateProvider: false,
|
|
3040
|
+
path: "A"
|
|
2951
3041
|
};
|
|
2952
3042
|
}
|
|
2953
3043
|
}
|
|
@@ -2962,14 +3052,18 @@ async function executeCycle(ctx) {
|
|
|
2962
3052
|
return {
|
|
2963
3053
|
output: statusline,
|
|
2964
3054
|
exitCode: 0,
|
|
2965
|
-
cacheUpdate: null
|
|
3055
|
+
cacheUpdate: null,
|
|
3056
|
+
invalidateProvider: false,
|
|
3057
|
+
path: "B2"
|
|
2966
3058
|
};
|
|
2967
3059
|
}
|
|
2968
3060
|
const errorOutput = renderError("endpoint-config-changed", "without-cache");
|
|
2969
3061
|
return {
|
|
2970
3062
|
output: errorOutput,
|
|
2971
3063
|
exitCode: 0,
|
|
2972
|
-
cacheUpdate: null
|
|
3064
|
+
cacheUpdate: null,
|
|
3065
|
+
invalidateProvider: false,
|
|
3066
|
+
path: "B2"
|
|
2973
3067
|
};
|
|
2974
3068
|
}
|
|
2975
3069
|
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
@@ -2984,7 +3078,9 @@ async function executeCycle(ctx) {
|
|
|
2984
3078
|
return {
|
|
2985
3079
|
output: statusline,
|
|
2986
3080
|
exitCode: 0,
|
|
2987
|
-
cacheUpdate: updatedEntry
|
|
3081
|
+
cacheUpdate: updatedEntry,
|
|
3082
|
+
invalidateProvider: false,
|
|
3083
|
+
path: "B"
|
|
2988
3084
|
};
|
|
2989
3085
|
}
|
|
2990
3086
|
const deadline = startTime + timeoutBudgetMs - EXIT_BUFFER_MS;
|
|
@@ -2995,7 +3091,9 @@ async function executeCycle(ctx) {
|
|
|
2995
3091
|
return {
|
|
2996
3092
|
output: errorOutput,
|
|
2997
3093
|
exitCode: 0,
|
|
2998
|
-
cacheUpdate: null
|
|
3094
|
+
cacheUpdate: null,
|
|
3095
|
+
invalidateProvider: false,
|
|
3096
|
+
path: "D"
|
|
2999
3097
|
};
|
|
3000
3098
|
}
|
|
3001
3099
|
try {
|
|
@@ -3005,7 +3103,9 @@ async function executeCycle(ctx) {
|
|
|
3005
3103
|
return {
|
|
3006
3104
|
output: renderError("missing-env", "without-cache"),
|
|
3007
3105
|
exitCode: 0,
|
|
3008
|
-
cacheUpdate: null
|
|
3106
|
+
cacheUpdate: null,
|
|
3107
|
+
invalidateProvider: false,
|
|
3108
|
+
path: "D"
|
|
3009
3109
|
};
|
|
3010
3110
|
}
|
|
3011
3111
|
logger.debug("Path C: Fetching from provider", { providerId, fetchTimeoutMs });
|
|
@@ -3031,23 +3131,30 @@ async function executeCycle(ctx) {
|
|
|
3031
3131
|
return {
|
|
3032
3132
|
output: statusline,
|
|
3033
3133
|
exitCode: 0,
|
|
3034
|
-
cacheUpdate: newEntry
|
|
3134
|
+
cacheUpdate: newEntry,
|
|
3135
|
+
invalidateProvider: false,
|
|
3136
|
+
path: "C"
|
|
3035
3137
|
};
|
|
3036
3138
|
} catch (error) {
|
|
3037
3139
|
logger.error("Path D: Fetch error", { error: String(error), hasCachedEntry: !!cachedEntry });
|
|
3140
|
+
const errorCategory = classifyFetchError(error);
|
|
3038
3141
|
let errorState = "network-error";
|
|
3039
3142
|
if (error && typeof error === "object" && "statusCode" in error) {
|
|
3040
3143
|
const statusCode = error.statusCode;
|
|
3041
3144
|
if (statusCode === 429) {
|
|
3042
3145
|
errorState = "rate-limited";
|
|
3146
|
+
} else if (errorCategory === "site-closed") {
|
|
3147
|
+
errorState = "network-error";
|
|
3043
3148
|
} else if (statusCode && statusCode >= 500) {
|
|
3044
3149
|
errorState = "server-error";
|
|
3045
3150
|
} else if (statusCode === 401 || statusCode === 403) {
|
|
3046
3151
|
errorState = "auth-error";
|
|
3047
3152
|
}
|
|
3153
|
+
} else if (errorCategory === "provider-mismatch") {
|
|
3154
|
+
errorState = "parse-error";
|
|
3048
3155
|
}
|
|
3049
3156
|
if (cachedEntry) {
|
|
3050
|
-
logger.debug("Discarding stale cache, showing error", { errorState });
|
|
3157
|
+
logger.debug("Discarding stale cache, showing error", { errorState, errorCategory });
|
|
3051
3158
|
} else {
|
|
3052
3159
|
logger.warn("No cache available for error fallback");
|
|
3053
3160
|
}
|
|
@@ -3055,10 +3162,32 @@ async function executeCycle(ctx) {
|
|
|
3055
3162
|
return {
|
|
3056
3163
|
output: errorOutput,
|
|
3057
3164
|
exitCode: 0,
|
|
3058
|
-
cacheUpdate: null
|
|
3165
|
+
cacheUpdate: null,
|
|
3166
|
+
invalidateProvider: errorCategory === "provider-mismatch",
|
|
3167
|
+
path: "D"
|
|
3059
3168
|
};
|
|
3060
3169
|
}
|
|
3061
3170
|
}
|
|
3171
|
+
// src/core/maintenance-scheduler.ts
|
|
3172
|
+
function selectMaintenanceTask(ctx) {
|
|
3173
|
+
if (ctx.path !== "A" && ctx.path !== "B")
|
|
3174
|
+
return "none";
|
|
3175
|
+
if (ctx.detectionCacheAgeMs === null)
|
|
3176
|
+
return "health-probe";
|
|
3177
|
+
if (ctx.detectionCacheAgeMs >= ctx.detectionCacheTtlMs * 0.5)
|
|
3178
|
+
return "health-probe";
|
|
3179
|
+
if (Math.random() < MAINTENANCE_GC_PROBABILITY)
|
|
3180
|
+
return "cache-gc";
|
|
3181
|
+
return "none";
|
|
3182
|
+
}
|
|
3183
|
+
function computeDynamicDetectionTtl(outcome, currentProvider, currentTtlSeconds) {
|
|
3184
|
+
if (!outcome.success)
|
|
3185
|
+
return DETECTION_TTL_FAILED_S;
|
|
3186
|
+
if (outcome.matchedProvider !== currentProvider)
|
|
3187
|
+
return DETECTION_TTL_CHANGED_S;
|
|
3188
|
+
return Math.min(currentTtlSeconds * 2, DETECTION_TTL_MAX_S);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3062
3191
|
// src/services/cache-gc.ts
|
|
3063
3192
|
import { readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync6, existsSync as existsSync6 } from "fs";
|
|
3064
3193
|
import { join as join12 } from "path";
|
|
@@ -3168,6 +3297,34 @@ function runCacheGC(cacheDir) {
|
|
|
3168
3297
|
}
|
|
3169
3298
|
|
|
3170
3299
|
// src/cli/piped-mode.ts
|
|
3300
|
+
var DEFAULT_PIPED_MODE_DEPS = {
|
|
3301
|
+
readCurrentEnv,
|
|
3302
|
+
validateRequiredEnv,
|
|
3303
|
+
readCache,
|
|
3304
|
+
writeCache,
|
|
3305
|
+
getCacheDir,
|
|
3306
|
+
isCacheValid,
|
|
3307
|
+
loadConfigWithHash,
|
|
3308
|
+
loadEndpointConfigs,
|
|
3309
|
+
computeEndpointConfigHash,
|
|
3310
|
+
readEndpointLock,
|
|
3311
|
+
writeEndpointLock,
|
|
3312
|
+
needsConfigInit,
|
|
3313
|
+
writeDefaultConfigs,
|
|
3314
|
+
resolveProvider,
|
|
3315
|
+
getProvider,
|
|
3316
|
+
invalidateDetectionCache,
|
|
3317
|
+
deleteProviderDetectionCache,
|
|
3318
|
+
renderError,
|
|
3319
|
+
dimText,
|
|
3320
|
+
executeCycle,
|
|
3321
|
+
logger,
|
|
3322
|
+
runCacheGC,
|
|
3323
|
+
probeHealthWithMetrics,
|
|
3324
|
+
readDetectionCacheMeta,
|
|
3325
|
+
cacheProviderDetectionWithTtl
|
|
3326
|
+
};
|
|
3327
|
+
|
|
3171
3328
|
class StatuslineError extends Error {
|
|
3172
3329
|
errorType;
|
|
3173
3330
|
constructor(errorType) {
|
|
@@ -3180,15 +3337,15 @@ function safeStdoutWrite(data) {
|
|
|
3180
3337
|
process.stdout["write"](data);
|
|
3181
3338
|
} catch {}
|
|
3182
3339
|
}
|
|
3183
|
-
function readAndValidateEnv() {
|
|
3184
|
-
const env = readCurrentEnv();
|
|
3185
|
-
logger.debug("Environment loaded", {
|
|
3340
|
+
function readAndValidateEnv(deps) {
|
|
3341
|
+
const env = deps.readCurrentEnv();
|
|
3342
|
+
deps.logger.debug("Environment loaded", {
|
|
3186
3343
|
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
3187
3344
|
hasToken: !!env.authToken,
|
|
3188
3345
|
providerOverride: env.providerOverride,
|
|
3189
3346
|
pollIntervalOverride: env.pollIntervalOverride
|
|
3190
3347
|
});
|
|
3191
|
-
const envError = validateRequiredEnv(env);
|
|
3348
|
+
const envError = deps.validateRequiredEnv(env);
|
|
3192
3349
|
if (envError) {
|
|
3193
3350
|
throw new StatuslineError("missing-env");
|
|
3194
3351
|
}
|
|
@@ -3198,75 +3355,75 @@ function readAndValidateEnv() {
|
|
|
3198
3355
|
}
|
|
3199
3356
|
return { env, baseUrl };
|
|
3200
3357
|
}
|
|
3201
|
-
function ensureDefaultConfigs() {
|
|
3202
|
-
if (needsConfigInit()) {
|
|
3203
|
-
logger.debug("First run detected - initializing default configs");
|
|
3204
|
-
writeDefaultConfigs();
|
|
3358
|
+
function ensureDefaultConfigs(deps) {
|
|
3359
|
+
if (deps.needsConfigInit()) {
|
|
3360
|
+
deps.logger.debug("First run detected - initializing default configs");
|
|
3361
|
+
deps.writeDefaultConfigs();
|
|
3205
3362
|
}
|
|
3206
3363
|
}
|
|
3207
|
-
function loadEndpointConfigsWithHash() {
|
|
3208
|
-
const endpointConfigs = loadEndpointConfigs();
|
|
3209
|
-
const endpointConfigHash = computeEndpointConfigHash();
|
|
3210
|
-
logger.debug("Endpoint configs loaded", {
|
|
3364
|
+
function loadEndpointConfigsWithHash(deps) {
|
|
3365
|
+
const endpointConfigs = deps.loadEndpointConfigs();
|
|
3366
|
+
const endpointConfigHash = deps.computeEndpointConfigHash();
|
|
3367
|
+
deps.logger.debug("Endpoint configs loaded", {
|
|
3211
3368
|
configCount: Object.keys(endpointConfigs).length,
|
|
3212
3369
|
endpointConfigHash
|
|
3213
3370
|
});
|
|
3214
3371
|
return { endpointConfigs, endpointConfigHash };
|
|
3215
3372
|
}
|
|
3216
|
-
function resolveEndpointLock(hash) {
|
|
3217
|
-
const existing = readEndpointLock();
|
|
3373
|
+
function resolveEndpointLock(hash, deps) {
|
|
3374
|
+
const existing = deps.readEndpointLock();
|
|
3218
3375
|
if (existing) {
|
|
3219
|
-
logger.debug("Endpoint lock file loaded", {
|
|
3376
|
+
deps.logger.debug("Endpoint lock file loaded", {
|
|
3220
3377
|
lockedHash: existing.hash,
|
|
3221
3378
|
currentHash: hash,
|
|
3222
3379
|
locked: existing.hash === hash
|
|
3223
3380
|
});
|
|
3224
3381
|
return existing;
|
|
3225
3382
|
}
|
|
3226
|
-
logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3227
|
-
writeEndpointLock(hash);
|
|
3383
|
+
deps.logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3384
|
+
deps.writeEndpointLock(hash);
|
|
3228
3385
|
return { hash, lockedAt: new Date().toISOString() };
|
|
3229
3386
|
}
|
|
3230
|
-
async function resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, timeoutMs) {
|
|
3231
|
-
const probeTimeout = isPiped ? Math.
|
|
3232
|
-
const providerId = await resolveProvider(baseUrl, env.providerOverride, endpointConfigs, probeTimeout);
|
|
3233
|
-
const provider = getProvider(providerId, endpointConfigs);
|
|
3234
|
-
logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
3387
|
+
async function resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, timeoutMs, deps) {
|
|
3388
|
+
const probeTimeout = isPiped ? Math.floor(timeoutMs / 2) : timeoutMs;
|
|
3389
|
+
const providerId = await deps.resolveProvider(baseUrl, env.providerOverride, endpointConfigs, probeTimeout);
|
|
3390
|
+
const provider = deps.getProvider(providerId, endpointConfigs);
|
|
3391
|
+
deps.logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
3235
3392
|
if (!provider) {
|
|
3236
|
-
logger.error("Provider not found", { providerId });
|
|
3393
|
+
deps.logger.error("Provider not found", { providerId });
|
|
3237
3394
|
throw new StatuslineError("provider-unknown");
|
|
3238
3395
|
}
|
|
3239
3396
|
return { providerId, provider };
|
|
3240
3397
|
}
|
|
3241
3398
|
function computeTimeoutBudgets(isPiped, config, timeoutMs) {
|
|
3242
|
-
const timeoutBudgetMs = isPiped ? timeoutMs :
|
|
3243
|
-
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ??
|
|
3399
|
+
const timeoutBudgetMs = isPiped ? timeoutMs : TTY_TIMEOUT_BUDGET_MS;
|
|
3400
|
+
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? DEFAULT_TIMEOUT_BUDGET_MS, timeoutBudgetMs - TIMEOUT_HEADROOM_MS) : TTY_TIMEOUT_BUDGET_MS;
|
|
3244
3401
|
return { timeoutBudgetMs, fetchTimeoutMs };
|
|
3245
3402
|
}
|
|
3246
|
-
async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs) {
|
|
3247
|
-
const { env, baseUrl } = readAndValidateEnv();
|
|
3248
|
-
ensureDefaultConfigs();
|
|
3249
|
-
const { config, configHash } = loadConfigWithHash(args.configPath);
|
|
3250
|
-
const { endpointConfigs, endpointConfigHash } = loadEndpointConfigsWithHash();
|
|
3251
|
-
const endpointLock = resolveEndpointLock(endpointConfigHash);
|
|
3252
|
-
const cachedEntry = readCache(baseUrl);
|
|
3253
|
-
logger.debug("Cache read", {
|
|
3403
|
+
async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs, deps) {
|
|
3404
|
+
const { env, baseUrl } = readAndValidateEnv(deps);
|
|
3405
|
+
ensureDefaultConfigs(deps);
|
|
3406
|
+
const { config, configHash } = deps.loadConfigWithHash(args.configPath);
|
|
3407
|
+
const { endpointConfigs, endpointConfigHash } = loadEndpointConfigsWithHash(deps);
|
|
3408
|
+
const endpointLock = resolveEndpointLock(endpointConfigHash, deps);
|
|
3409
|
+
const cachedEntry = deps.readCache(baseUrl);
|
|
3410
|
+
deps.logger.debug("Cache read", {
|
|
3254
3411
|
cacheHit: !!cachedEntry,
|
|
3255
3412
|
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
3256
3413
|
});
|
|
3257
3414
|
let providerId;
|
|
3258
3415
|
let provider;
|
|
3259
|
-
if (cachedEntry && isCacheValid(cachedEntry, env)) {
|
|
3260
|
-
const cachedProvider = getProvider(cachedEntry.provider, endpointConfigs);
|
|
3416
|
+
if (cachedEntry && deps.isCacheValid(cachedEntry, env)) {
|
|
3417
|
+
const cachedProvider = deps.getProvider(cachedEntry.provider, endpointConfigs);
|
|
3261
3418
|
if (cachedProvider) {
|
|
3262
3419
|
providerId = cachedEntry.provider;
|
|
3263
3420
|
provider = cachedProvider;
|
|
3264
|
-
logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3421
|
+
deps.logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3265
3422
|
} else {
|
|
3266
|
-
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3423
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs, deps));
|
|
3267
3424
|
}
|
|
3268
3425
|
} else {
|
|
3269
|
-
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3426
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs, deps));
|
|
3270
3427
|
}
|
|
3271
3428
|
const { timeoutBudgetMs, fetchTimeoutMs } = computeTimeoutBudgets(isPiped, config, rawTimeoutMs);
|
|
3272
3429
|
const ctx = {
|
|
@@ -3282,82 +3439,124 @@ async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs) {
|
|
|
3282
3439
|
startTime,
|
|
3283
3440
|
fetchTimeoutMs
|
|
3284
3441
|
};
|
|
3285
|
-
return { ctx, baseUrl };
|
|
3442
|
+
return { ctx, baseUrl, endpointConfigs };
|
|
3286
3443
|
}
|
|
3287
|
-
function formatOutput(output,
|
|
3444
|
+
function formatOutput(output, mode, log) {
|
|
3288
3445
|
let normalizedOutput = output;
|
|
3289
3446
|
if (!normalizedOutput || normalizedOutput.trim().length === 0) {
|
|
3290
|
-
|
|
3447
|
+
log.debug("Empty output detected, using fallback");
|
|
3291
3448
|
normalizedOutput = "[loading...]";
|
|
3292
3449
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3450
|
+
switch (mode) {
|
|
3451
|
+
case "piped-embedded":
|
|
3452
|
+
log.debug("Output written (embedded piped mode - no host formatting)");
|
|
3453
|
+
return normalizedOutput;
|
|
3454
|
+
case "piped":
|
|
3455
|
+
log.debug("Output formatted for piped mode (ANSI reset + NBSP)");
|
|
3456
|
+
return "\x1B[0m" + normalizedOutput.replace(/ /g, " ");
|
|
3457
|
+
case "tty":
|
|
3458
|
+
log.debug("Output written (TTY mode)");
|
|
3459
|
+
return normalizedOutput + `
|
|
3460
|
+
`;
|
|
3299
3461
|
}
|
|
3300
3462
|
}
|
|
3301
|
-
async function
|
|
3463
|
+
async function runMaintenance(result, baseUrl, startTime, budgetMs, endpointConfigs, currentProviderId, deps) {
|
|
3464
|
+
const { ageMs, ttlMs } = deps.readDetectionCacheMeta(baseUrl);
|
|
3465
|
+
const task = selectMaintenanceTask({
|
|
3466
|
+
path: result.path,
|
|
3467
|
+
detectionCacheAgeMs: ageMs,
|
|
3468
|
+
detectionCacheTtlMs: ttlMs
|
|
3469
|
+
});
|
|
3470
|
+
if (task === "none")
|
|
3471
|
+
return;
|
|
3472
|
+
deps.logger.debug("Maintenance task selected", { task, path: result.path });
|
|
3473
|
+
if (task === "health-probe") {
|
|
3474
|
+
const elapsed = Date.now() - startTime;
|
|
3475
|
+
const remainingMs = Math.max(50, budgetMs - elapsed - TIMEOUT_HEADROOM_MS);
|
|
3476
|
+
const currentTtlS = Math.floor(ttlMs / 1000) || DETECTION_TTL_BASE_S;
|
|
3477
|
+
const outcome = await deps.probeHealthWithMetrics(baseUrl, remainingMs, endpointConfigs);
|
|
3478
|
+
deps.logger.debug("Maintenance probe completed", {
|
|
3479
|
+
success: outcome.success,
|
|
3480
|
+
matchedProvider: outcome.matchedProvider,
|
|
3481
|
+
responseTimeMs: outcome.responseTimeMs
|
|
3482
|
+
});
|
|
3483
|
+
if (outcome.success && outcome.matchedProvider) {
|
|
3484
|
+
const newTtlS = computeDynamicDetectionTtl(outcome, currentProviderId, currentTtlS);
|
|
3485
|
+
deps.cacheProviderDetectionWithTtl(baseUrl, outcome.matchedProvider, newTtlS);
|
|
3486
|
+
deps.logger.debug("Detection cache refreshed", { ttlSeconds: newTtlS, provider: outcome.matchedProvider });
|
|
3487
|
+
}
|
|
3488
|
+
} else if (task === "cache-gc") {
|
|
3489
|
+
deps.runCacheGC(deps.getCacheDir());
|
|
3490
|
+
deps.logger.debug("Cache GC completed");
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
async function executePipedMode(args, deps = DEFAULT_PIPED_MODE_DEPS) {
|
|
3302
3494
|
const startTime = Date.now();
|
|
3303
|
-
logger.debug("=== cc-api-statusline execution started ===");
|
|
3304
|
-
logger.debug("Start time", { startTime });
|
|
3495
|
+
deps.logger.debug("=== cc-api-statusline execution started ===");
|
|
3496
|
+
deps.logger.debug("Start time", { startTime });
|
|
3305
3497
|
const isPiped = !process.stdin.isTTY;
|
|
3306
|
-
|
|
3307
|
-
|
|
3498
|
+
const outputMode = !isPiped ? "tty" : args.embedded ? "piped-embedded" : "piped";
|
|
3499
|
+
deps.logger.debug("Mode detection", { isPiped, once: args.once, outputMode });
|
|
3500
|
+
const rawTimeoutMs = Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? DEFAULT_TIMEOUT_BUDGET_MS);
|
|
3308
3501
|
if (isPiped) {
|
|
3309
|
-
const watchdogMs = rawTimeoutMs -
|
|
3502
|
+
const watchdogMs = rawTimeoutMs - TIMEOUT_HEADROOM_MS;
|
|
3310
3503
|
setTimeout(() => {
|
|
3311
|
-
logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
|
|
3312
|
-
const fallback = dimText("⟳ Refreshing...");
|
|
3313
|
-
const formatted = formatOutput(fallback,
|
|
3504
|
+
deps.logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
|
|
3505
|
+
const fallback = deps.dimText("⟳ Refreshing...");
|
|
3506
|
+
const formatted = formatOutput(fallback, outputMode, deps.logger);
|
|
3314
3507
|
safeStdoutWrite(formatted);
|
|
3315
3508
|
process.exit(0);
|
|
3316
3509
|
}, watchdogMs).unref();
|
|
3317
3510
|
}
|
|
3318
3511
|
let ctx;
|
|
3319
3512
|
let baseUrl;
|
|
3513
|
+
let endpointConfigs;
|
|
3320
3514
|
try {
|
|
3321
|
-
({ ctx, baseUrl } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs));
|
|
3515
|
+
({ ctx, baseUrl, endpointConfigs } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs, deps));
|
|
3322
3516
|
} catch (error) {
|
|
3323
|
-
logger.error("Failed to build execution context", { error: String(error) });
|
|
3517
|
+
deps.logger.error("Failed to build execution context", { error: String(error) });
|
|
3324
3518
|
const errorType = error instanceof StatuslineError ? error.errorType : "network-error";
|
|
3325
|
-
const errorOutput = renderError(errorType, "without-cache");
|
|
3326
|
-
const formattedOutput2 = formatOutput(errorOutput,
|
|
3519
|
+
const errorOutput = deps.renderError(errorType, "without-cache");
|
|
3520
|
+
const formattedOutput2 = formatOutput(errorOutput, outputMode, deps.logger);
|
|
3327
3521
|
safeStdoutWrite(formattedOutput2);
|
|
3328
|
-
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3522
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3329
3523
|
process.exit(0);
|
|
3330
3524
|
}
|
|
3331
|
-
logger.debug("Execution context prepared", {
|
|
3525
|
+
deps.logger.debug("Execution context prepared", {
|
|
3332
3526
|
timeoutBudgetMs: ctx.timeoutBudgetMs,
|
|
3333
3527
|
fetchTimeoutMs: ctx.fetchTimeoutMs
|
|
3334
3528
|
});
|
|
3335
3529
|
let result;
|
|
3336
3530
|
try {
|
|
3337
|
-
result = await executeCycle(ctx);
|
|
3531
|
+
result = await deps.executeCycle(ctx);
|
|
3338
3532
|
} catch (error) {
|
|
3339
|
-
logger.error("Execution cycle failed", { error: String(error) });
|
|
3340
|
-
const errorOutput = renderError("network-error", "without-cache");
|
|
3341
|
-
const formattedOutput2 = formatOutput(errorOutput,
|
|
3533
|
+
deps.logger.error("Execution cycle failed", { error: String(error) });
|
|
3534
|
+
const errorOutput = deps.renderError("network-error", "without-cache");
|
|
3535
|
+
const formattedOutput2 = formatOutput(errorOutput, outputMode, deps.logger);
|
|
3342
3536
|
safeStdoutWrite(formattedOutput2);
|
|
3343
|
-
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3537
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3344
3538
|
process.exit(0);
|
|
3345
3539
|
}
|
|
3346
3540
|
const executionTime = Date.now() - startTime;
|
|
3347
|
-
logger.debug("Execution completed", {
|
|
3541
|
+
deps.logger.debug("Execution completed", {
|
|
3348
3542
|
exitCode: result.exitCode,
|
|
3349
3543
|
executionTime: `${executionTime}ms`,
|
|
3350
3544
|
outputLength: result.output.length,
|
|
3351
3545
|
cacheUpdate: !!result.cacheUpdate
|
|
3352
3546
|
});
|
|
3353
|
-
const formattedOutput = formatOutput(result.output,
|
|
3547
|
+
const formattedOutput = formatOutput(result.output, outputMode, deps.logger);
|
|
3354
3548
|
safeStdoutWrite(formattedOutput);
|
|
3549
|
+
if (result.invalidateProvider) {
|
|
3550
|
+
deps.invalidateDetectionCache(baseUrl);
|
|
3551
|
+
deps.deleteProviderDetectionCache(baseUrl);
|
|
3552
|
+
deps.logger.debug("Provider detection cache invalidated", { baseUrl });
|
|
3553
|
+
}
|
|
3355
3554
|
if (result.cacheUpdate) {
|
|
3356
|
-
writeCache(baseUrl, result.cacheUpdate);
|
|
3357
|
-
logger.debug("Cache written", { baseUrl });
|
|
3358
|
-
runCacheGC(getCacheDir());
|
|
3555
|
+
deps.writeCache(baseUrl, result.cacheUpdate);
|
|
3556
|
+
deps.logger.debug("Cache written", { baseUrl });
|
|
3359
3557
|
}
|
|
3360
|
-
|
|
3558
|
+
await runMaintenance(result, baseUrl, startTime, rawTimeoutMs, endpointConfigs, ctx.providerId, deps);
|
|
3559
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3361
3560
|
process.exit(result.exitCode);
|
|
3362
3561
|
}
|
|
3363
3562
|
// src/main.ts
|