cc-api-statusline 1.0.2 → 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 +28 -30
- package/README.zh-CN.md +23 -28
- package/dist/cc-api-statusline.js +345 -151
- 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.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,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}`);
|
|
@@ -2935,6 +2989,35 @@ function isComponentId(key) {
|
|
|
2935
2989
|
return DEFAULT_COMPONENT_ORDER.includes(key);
|
|
2936
2990
|
}
|
|
2937
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
|
+
|
|
2938
3021
|
// src/core/execute-cycle.ts
|
|
2939
3022
|
async function executeCycle(ctx) {
|
|
2940
3023
|
const { env, config, configHash, endpointConfigHash, endpointLock, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
@@ -2947,7 +3030,9 @@ async function executeCycle(ctx) {
|
|
|
2947
3030
|
return {
|
|
2948
3031
|
output: cachedEntry.renderedLine,
|
|
2949
3032
|
exitCode: 0,
|
|
2950
|
-
cacheUpdate: null
|
|
3033
|
+
cacheUpdate: null,
|
|
3034
|
+
invalidateProvider: false,
|
|
3035
|
+
path: "A"
|
|
2951
3036
|
};
|
|
2952
3037
|
}
|
|
2953
3038
|
}
|
|
@@ -2962,14 +3047,18 @@ async function executeCycle(ctx) {
|
|
|
2962
3047
|
return {
|
|
2963
3048
|
output: statusline,
|
|
2964
3049
|
exitCode: 0,
|
|
2965
|
-
cacheUpdate: null
|
|
3050
|
+
cacheUpdate: null,
|
|
3051
|
+
invalidateProvider: false,
|
|
3052
|
+
path: "B2"
|
|
2966
3053
|
};
|
|
2967
3054
|
}
|
|
2968
3055
|
const errorOutput = renderError("endpoint-config-changed", "without-cache");
|
|
2969
3056
|
return {
|
|
2970
3057
|
output: errorOutput,
|
|
2971
3058
|
exitCode: 0,
|
|
2972
|
-
cacheUpdate: null
|
|
3059
|
+
cacheUpdate: null,
|
|
3060
|
+
invalidateProvider: false,
|
|
3061
|
+
path: "B2"
|
|
2973
3062
|
};
|
|
2974
3063
|
}
|
|
2975
3064
|
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
@@ -2984,7 +3073,9 @@ async function executeCycle(ctx) {
|
|
|
2984
3073
|
return {
|
|
2985
3074
|
output: statusline,
|
|
2986
3075
|
exitCode: 0,
|
|
2987
|
-
cacheUpdate: updatedEntry
|
|
3076
|
+
cacheUpdate: updatedEntry,
|
|
3077
|
+
invalidateProvider: false,
|
|
3078
|
+
path: "B"
|
|
2988
3079
|
};
|
|
2989
3080
|
}
|
|
2990
3081
|
const deadline = startTime + timeoutBudgetMs - EXIT_BUFFER_MS;
|
|
@@ -2995,7 +3086,9 @@ async function executeCycle(ctx) {
|
|
|
2995
3086
|
return {
|
|
2996
3087
|
output: errorOutput,
|
|
2997
3088
|
exitCode: 0,
|
|
2998
|
-
cacheUpdate: null
|
|
3089
|
+
cacheUpdate: null,
|
|
3090
|
+
invalidateProvider: false,
|
|
3091
|
+
path: "D"
|
|
2999
3092
|
};
|
|
3000
3093
|
}
|
|
3001
3094
|
try {
|
|
@@ -3005,7 +3098,9 @@ async function executeCycle(ctx) {
|
|
|
3005
3098
|
return {
|
|
3006
3099
|
output: renderError("missing-env", "without-cache"),
|
|
3007
3100
|
exitCode: 0,
|
|
3008
|
-
cacheUpdate: null
|
|
3101
|
+
cacheUpdate: null,
|
|
3102
|
+
invalidateProvider: false,
|
|
3103
|
+
path: "D"
|
|
3009
3104
|
};
|
|
3010
3105
|
}
|
|
3011
3106
|
logger.debug("Path C: Fetching from provider", { providerId, fetchTimeoutMs });
|
|
@@ -3031,23 +3126,30 @@ async function executeCycle(ctx) {
|
|
|
3031
3126
|
return {
|
|
3032
3127
|
output: statusline,
|
|
3033
3128
|
exitCode: 0,
|
|
3034
|
-
cacheUpdate: newEntry
|
|
3129
|
+
cacheUpdate: newEntry,
|
|
3130
|
+
invalidateProvider: false,
|
|
3131
|
+
path: "C"
|
|
3035
3132
|
};
|
|
3036
3133
|
} catch (error) {
|
|
3037
3134
|
logger.error("Path D: Fetch error", { error: String(error), hasCachedEntry: !!cachedEntry });
|
|
3135
|
+
const errorCategory = classifyFetchError(error);
|
|
3038
3136
|
let errorState = "network-error";
|
|
3039
3137
|
if (error && typeof error === "object" && "statusCode" in error) {
|
|
3040
3138
|
const statusCode = error.statusCode;
|
|
3041
3139
|
if (statusCode === 429) {
|
|
3042
3140
|
errorState = "rate-limited";
|
|
3141
|
+
} else if (errorCategory === "site-closed") {
|
|
3142
|
+
errorState = "network-error";
|
|
3043
3143
|
} else if (statusCode && statusCode >= 500) {
|
|
3044
3144
|
errorState = "server-error";
|
|
3045
3145
|
} else if (statusCode === 401 || statusCode === 403) {
|
|
3046
3146
|
errorState = "auth-error";
|
|
3047
3147
|
}
|
|
3148
|
+
} else if (errorCategory === "provider-mismatch") {
|
|
3149
|
+
errorState = "parse-error";
|
|
3048
3150
|
}
|
|
3049
3151
|
if (cachedEntry) {
|
|
3050
|
-
logger.debug("Discarding stale cache, showing error", { errorState });
|
|
3152
|
+
logger.debug("Discarding stale cache, showing error", { errorState, errorCategory });
|
|
3051
3153
|
} else {
|
|
3052
3154
|
logger.warn("No cache available for error fallback");
|
|
3053
3155
|
}
|
|
@@ -3055,10 +3157,32 @@ async function executeCycle(ctx) {
|
|
|
3055
3157
|
return {
|
|
3056
3158
|
output: errorOutput,
|
|
3057
3159
|
exitCode: 0,
|
|
3058
|
-
cacheUpdate: null
|
|
3160
|
+
cacheUpdate: null,
|
|
3161
|
+
invalidateProvider: errorCategory === "provider-mismatch",
|
|
3162
|
+
path: "D"
|
|
3059
3163
|
};
|
|
3060
3164
|
}
|
|
3061
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
|
+
|
|
3062
3186
|
// src/services/cache-gc.ts
|
|
3063
3187
|
import { readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync6, existsSync as existsSync6 } from "fs";
|
|
3064
3188
|
import { join as join12 } from "path";
|
|
@@ -3168,6 +3292,34 @@ function runCacheGC(cacheDir) {
|
|
|
3168
3292
|
}
|
|
3169
3293
|
|
|
3170
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
|
+
|
|
3171
3323
|
class StatuslineError extends Error {
|
|
3172
3324
|
errorType;
|
|
3173
3325
|
constructor(errorType) {
|
|
@@ -3180,15 +3332,15 @@ function safeStdoutWrite(data) {
|
|
|
3180
3332
|
process.stdout["write"](data);
|
|
3181
3333
|
} catch {}
|
|
3182
3334
|
}
|
|
3183
|
-
function readAndValidateEnv() {
|
|
3184
|
-
const env = readCurrentEnv();
|
|
3185
|
-
logger.debug("Environment loaded", {
|
|
3335
|
+
function readAndValidateEnv(deps) {
|
|
3336
|
+
const env = deps.readCurrentEnv();
|
|
3337
|
+
deps.logger.debug("Environment loaded", {
|
|
3186
3338
|
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
3187
3339
|
hasToken: !!env.authToken,
|
|
3188
3340
|
providerOverride: env.providerOverride,
|
|
3189
3341
|
pollIntervalOverride: env.pollIntervalOverride
|
|
3190
3342
|
});
|
|
3191
|
-
const envError = validateRequiredEnv(env);
|
|
3343
|
+
const envError = deps.validateRequiredEnv(env);
|
|
3192
3344
|
if (envError) {
|
|
3193
3345
|
throw new StatuslineError("missing-env");
|
|
3194
3346
|
}
|
|
@@ -3198,75 +3350,75 @@ function readAndValidateEnv() {
|
|
|
3198
3350
|
}
|
|
3199
3351
|
return { env, baseUrl };
|
|
3200
3352
|
}
|
|
3201
|
-
function ensureDefaultConfigs() {
|
|
3202
|
-
if (needsConfigInit()) {
|
|
3203
|
-
logger.debug("First run detected - initializing default configs");
|
|
3204
|
-
writeDefaultConfigs();
|
|
3353
|
+
function ensureDefaultConfigs(deps) {
|
|
3354
|
+
if (deps.needsConfigInit()) {
|
|
3355
|
+
deps.logger.debug("First run detected - initializing default configs");
|
|
3356
|
+
deps.writeDefaultConfigs();
|
|
3205
3357
|
}
|
|
3206
3358
|
}
|
|
3207
|
-
function loadEndpointConfigsWithHash() {
|
|
3208
|
-
const endpointConfigs = loadEndpointConfigs();
|
|
3209
|
-
const endpointConfigHash = computeEndpointConfigHash();
|
|
3210
|
-
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", {
|
|
3211
3363
|
configCount: Object.keys(endpointConfigs).length,
|
|
3212
3364
|
endpointConfigHash
|
|
3213
3365
|
});
|
|
3214
3366
|
return { endpointConfigs, endpointConfigHash };
|
|
3215
3367
|
}
|
|
3216
|
-
function resolveEndpointLock(hash) {
|
|
3217
|
-
const existing = readEndpointLock();
|
|
3368
|
+
function resolveEndpointLock(hash, deps) {
|
|
3369
|
+
const existing = deps.readEndpointLock();
|
|
3218
3370
|
if (existing) {
|
|
3219
|
-
logger.debug("Endpoint lock file loaded", {
|
|
3371
|
+
deps.logger.debug("Endpoint lock file loaded", {
|
|
3220
3372
|
lockedHash: existing.hash,
|
|
3221
3373
|
currentHash: hash,
|
|
3222
3374
|
locked: existing.hash === hash
|
|
3223
3375
|
});
|
|
3224
3376
|
return existing;
|
|
3225
3377
|
}
|
|
3226
|
-
logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3227
|
-
writeEndpointLock(hash);
|
|
3378
|
+
deps.logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3379
|
+
deps.writeEndpointLock(hash);
|
|
3228
3380
|
return { hash, lockedAt: new Date().toISOString() };
|
|
3229
3381
|
}
|
|
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 });
|
|
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 });
|
|
3235
3387
|
if (!provider) {
|
|
3236
|
-
logger.error("Provider not found", { providerId });
|
|
3388
|
+
deps.logger.error("Provider not found", { providerId });
|
|
3237
3389
|
throw new StatuslineError("provider-unknown");
|
|
3238
3390
|
}
|
|
3239
3391
|
return { providerId, provider };
|
|
3240
3392
|
}
|
|
3241
3393
|
function computeTimeoutBudgets(isPiped, config, timeoutMs) {
|
|
3242
|
-
const timeoutBudgetMs = isPiped ? timeoutMs :
|
|
3243
|
-
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;
|
|
3244
3396
|
return { timeoutBudgetMs, fetchTimeoutMs };
|
|
3245
3397
|
}
|
|
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", {
|
|
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", {
|
|
3254
3406
|
cacheHit: !!cachedEntry,
|
|
3255
3407
|
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
3256
3408
|
});
|
|
3257
3409
|
let providerId;
|
|
3258
3410
|
let provider;
|
|
3259
|
-
if (cachedEntry && isCacheValid(cachedEntry, env)) {
|
|
3260
|
-
const cachedProvider = getProvider(cachedEntry.provider, endpointConfigs);
|
|
3411
|
+
if (cachedEntry && deps.isCacheValid(cachedEntry, env)) {
|
|
3412
|
+
const cachedProvider = deps.getProvider(cachedEntry.provider, endpointConfigs);
|
|
3261
3413
|
if (cachedProvider) {
|
|
3262
3414
|
providerId = cachedEntry.provider;
|
|
3263
3415
|
provider = cachedProvider;
|
|
3264
|
-
logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3416
|
+
deps.logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3265
3417
|
} else {
|
|
3266
|
-
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3418
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs, deps));
|
|
3267
3419
|
}
|
|
3268
3420
|
} else {
|
|
3269
|
-
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3421
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs, deps));
|
|
3270
3422
|
}
|
|
3271
3423
|
const { timeoutBudgetMs, fetchTimeoutMs } = computeTimeoutBudgets(isPiped, config, rawTimeoutMs);
|
|
3272
3424
|
const ctx = {
|
|
@@ -3282,82 +3434,124 @@ async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs) {
|
|
|
3282
3434
|
startTime,
|
|
3283
3435
|
fetchTimeoutMs
|
|
3284
3436
|
};
|
|
3285
|
-
return { ctx, baseUrl };
|
|
3437
|
+
return { ctx, baseUrl, endpointConfigs };
|
|
3286
3438
|
}
|
|
3287
|
-
function formatOutput(output,
|
|
3439
|
+
function formatOutput(output, mode, log) {
|
|
3288
3440
|
let normalizedOutput = output;
|
|
3289
3441
|
if (!normalizedOutput || normalizedOutput.trim().length === 0) {
|
|
3290
|
-
|
|
3442
|
+
log.debug("Empty output detected, using fallback");
|
|
3291
3443
|
normalizedOutput = "[loading...]";
|
|
3292
3444
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
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
|
+
`;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
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");
|
|
3299
3486
|
}
|
|
3300
3487
|
}
|
|
3301
|
-
async function executePipedMode(args) {
|
|
3488
|
+
async function executePipedMode(args, deps = DEFAULT_PIPED_MODE_DEPS) {
|
|
3302
3489
|
const startTime = Date.now();
|
|
3303
|
-
logger.debug("=== cc-api-statusline execution started ===");
|
|
3304
|
-
logger.debug("Start time", { startTime });
|
|
3490
|
+
deps.logger.debug("=== cc-api-statusline execution started ===");
|
|
3491
|
+
deps.logger.debug("Start time", { startTime });
|
|
3305
3492
|
const isPiped = !process.stdin.isTTY;
|
|
3306
|
-
|
|
3307
|
-
|
|
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);
|
|
3308
3496
|
if (isPiped) {
|
|
3309
|
-
const watchdogMs = rawTimeoutMs -
|
|
3497
|
+
const watchdogMs = rawTimeoutMs - TIMEOUT_HEADROOM_MS;
|
|
3310
3498
|
setTimeout(() => {
|
|
3311
|
-
logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
|
|
3312
|
-
const fallback = dimText("⟳ Refreshing...");
|
|
3313
|
-
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);
|
|
3314
3502
|
safeStdoutWrite(formatted);
|
|
3315
3503
|
process.exit(0);
|
|
3316
3504
|
}, watchdogMs).unref();
|
|
3317
3505
|
}
|
|
3318
3506
|
let ctx;
|
|
3319
3507
|
let baseUrl;
|
|
3508
|
+
let endpointConfigs;
|
|
3320
3509
|
try {
|
|
3321
|
-
({ ctx, baseUrl } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs));
|
|
3510
|
+
({ ctx, baseUrl, endpointConfigs } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs, deps));
|
|
3322
3511
|
} catch (error) {
|
|
3323
|
-
logger.error("Failed to build execution context", { error: String(error) });
|
|
3512
|
+
deps.logger.error("Failed to build execution context", { error: String(error) });
|
|
3324
3513
|
const errorType = error instanceof StatuslineError ? error.errorType : "network-error";
|
|
3325
|
-
const errorOutput = renderError(errorType, "without-cache");
|
|
3326
|
-
const formattedOutput2 = formatOutput(errorOutput,
|
|
3514
|
+
const errorOutput = deps.renderError(errorType, "without-cache");
|
|
3515
|
+
const formattedOutput2 = formatOutput(errorOutput, outputMode, deps.logger);
|
|
3327
3516
|
safeStdoutWrite(formattedOutput2);
|
|
3328
|
-
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3517
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3329
3518
|
process.exit(0);
|
|
3330
3519
|
}
|
|
3331
|
-
logger.debug("Execution context prepared", {
|
|
3520
|
+
deps.logger.debug("Execution context prepared", {
|
|
3332
3521
|
timeoutBudgetMs: ctx.timeoutBudgetMs,
|
|
3333
3522
|
fetchTimeoutMs: ctx.fetchTimeoutMs
|
|
3334
3523
|
});
|
|
3335
3524
|
let result;
|
|
3336
3525
|
try {
|
|
3337
|
-
result = await executeCycle(ctx);
|
|
3526
|
+
result = await deps.executeCycle(ctx);
|
|
3338
3527
|
} catch (error) {
|
|
3339
|
-
logger.error("Execution cycle failed", { error: String(error) });
|
|
3340
|
-
const errorOutput = renderError("network-error", "without-cache");
|
|
3341
|
-
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);
|
|
3342
3531
|
safeStdoutWrite(formattedOutput2);
|
|
3343
|
-
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3532
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3344
3533
|
process.exit(0);
|
|
3345
3534
|
}
|
|
3346
3535
|
const executionTime = Date.now() - startTime;
|
|
3347
|
-
logger.debug("Execution completed", {
|
|
3536
|
+
deps.logger.debug("Execution completed", {
|
|
3348
3537
|
exitCode: result.exitCode,
|
|
3349
3538
|
executionTime: `${executionTime}ms`,
|
|
3350
3539
|
outputLength: result.output.length,
|
|
3351
3540
|
cacheUpdate: !!result.cacheUpdate
|
|
3352
3541
|
});
|
|
3353
|
-
const formattedOutput = formatOutput(result.output,
|
|
3542
|
+
const formattedOutput = formatOutput(result.output, outputMode, deps.logger);
|
|
3354
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
|
+
}
|
|
3355
3549
|
if (result.cacheUpdate) {
|
|
3356
|
-
writeCache(baseUrl, result.cacheUpdate);
|
|
3357
|
-
logger.debug("Cache written", { baseUrl });
|
|
3358
|
-
runCacheGC(getCacheDir());
|
|
3550
|
+
deps.writeCache(baseUrl, result.cacheUpdate);
|
|
3551
|
+
deps.logger.debug("Cache written", { baseUrl });
|
|
3359
3552
|
}
|
|
3360
|
-
|
|
3553
|
+
await runMaintenance(result, baseUrl, startTime, rawTimeoutMs, endpointConfigs, ctx.providerId, deps);
|
|
3554
|
+
deps.logger.debug("=== cc-api-statusline execution completed ===");
|
|
3361
3555
|
process.exit(result.exitCode);
|
|
3362
3556
|
}
|
|
3363
3557
|
// src/main.ts
|