cc-api-statusline 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +579 -0
- package/dist/cc-api-statusline.js +2257 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/services/env.ts
|
|
6
|
+
import { readFileSync, existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
|
|
10
|
+
// src/services/hash.ts
|
|
11
|
+
function sha256(input) {
|
|
12
|
+
if (typeof Bun !== "undefined" && typeof Bun.CryptoHasher !== "undefined") {
|
|
13
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
14
|
+
hasher.update(input);
|
|
15
|
+
return hasher.digest("hex");
|
|
16
|
+
}
|
|
17
|
+
const crypto = __require("crypto");
|
|
18
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
function shortHash(input, length = 12) {
|
|
21
|
+
const fullHash = sha256(input);
|
|
22
|
+
return fullHash.slice(0, length);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/services/env.ts
|
|
26
|
+
function getSettingsJsonPath() {
|
|
27
|
+
const configDir = process.env["CLAUDE_CONFIG_DIR"];
|
|
28
|
+
if (configDir) {
|
|
29
|
+
return join(configDir, "settings.json");
|
|
30
|
+
}
|
|
31
|
+
return join(homedir(), ".claude", "settings.json");
|
|
32
|
+
}
|
|
33
|
+
function readSettingsJsonEnv() {
|
|
34
|
+
const settingsPath = getSettingsJsonPath();
|
|
35
|
+
if (!existsSync(settingsPath)) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
40
|
+
const settings = JSON.parse(content);
|
|
41
|
+
if (settings["env"] && typeof settings["env"] === "object") {
|
|
42
|
+
const env = settings["env"];
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const [key, value] of Object.entries(env)) {
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
result[key] = value;
|
|
47
|
+
} else if (value !== null && value !== undefined && (typeof value === "number" || typeof value === "boolean")) {
|
|
48
|
+
result[key] = String(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(`Warning: Could not read settings.json: ${error}`);
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readCurrentEnv() {
|
|
60
|
+
const settingsEnv = readSettingsJsonEnv();
|
|
61
|
+
const getEnv = (key) => {
|
|
62
|
+
return settingsEnv[key] ?? process.env[key] ?? null;
|
|
63
|
+
};
|
|
64
|
+
const baseUrl = getEnv("ANTHROPIC_BASE_URL");
|
|
65
|
+
const authToken = getEnv("ANTHROPIC_AUTH_TOKEN");
|
|
66
|
+
const providerOverride = getEnv("CC_STATUSLINE_PROVIDER");
|
|
67
|
+
const pollIntervalRaw = getEnv("CC_STATUSLINE_POLL");
|
|
68
|
+
const tokenHash = authToken ? shortHash(authToken, 12) : null;
|
|
69
|
+
let pollIntervalOverride = null;
|
|
70
|
+
if (pollIntervalRaw) {
|
|
71
|
+
const parsed = parseInt(pollIntervalRaw, 10);
|
|
72
|
+
if (!isNaN(parsed) && parsed >= 5) {
|
|
73
|
+
pollIntervalOverride = parsed;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
baseUrl,
|
|
78
|
+
authToken,
|
|
79
|
+
tokenHash,
|
|
80
|
+
providerOverride,
|
|
81
|
+
pollIntervalOverride
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function validateRequiredEnv(env) {
|
|
85
|
+
if (!env.baseUrl) {
|
|
86
|
+
return "Missing required environment variable: ANTHROPIC_BASE_URL";
|
|
87
|
+
}
|
|
88
|
+
if (!env.authToken) {
|
|
89
|
+
return "Missing required environment variable: ANTHROPIC_AUTH_TOKEN";
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/services/cache.ts
|
|
95
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, unlinkSync, renameSync, chmodSync, mkdirSync } from "fs";
|
|
96
|
+
import { join as join2 } from "path";
|
|
97
|
+
import { homedir as homedir2 } from "os";
|
|
98
|
+
|
|
99
|
+
// src/types/normalized-usage.ts
|
|
100
|
+
function createEmptyNormalizedUsage(provider, billingMode, planName) {
|
|
101
|
+
return {
|
|
102
|
+
provider,
|
|
103
|
+
billingMode,
|
|
104
|
+
planName,
|
|
105
|
+
fetchedAt: new Date().toISOString(),
|
|
106
|
+
resetSemantics: "end-of-day",
|
|
107
|
+
daily: null,
|
|
108
|
+
weekly: null,
|
|
109
|
+
monthly: null,
|
|
110
|
+
balance: null,
|
|
111
|
+
resetsAt: null,
|
|
112
|
+
tokenStats: null,
|
|
113
|
+
rateLimit: null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function computeSoonestReset(usage) {
|
|
117
|
+
const times = [];
|
|
118
|
+
if (usage.daily?.resetsAt)
|
|
119
|
+
times.push(usage.daily.resetsAt);
|
|
120
|
+
if (usage.weekly?.resetsAt)
|
|
121
|
+
times.push(usage.weekly.resetsAt);
|
|
122
|
+
if (usage.monthly?.resetsAt)
|
|
123
|
+
times.push(usage.monthly.resetsAt);
|
|
124
|
+
if (times.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
times.sort();
|
|
127
|
+
return times[0] ?? null;
|
|
128
|
+
}
|
|
129
|
+
// src/types/config.ts
|
|
130
|
+
var DEFAULT_CONFIG = {
|
|
131
|
+
display: {
|
|
132
|
+
layout: "standard",
|
|
133
|
+
displayMode: "bar",
|
|
134
|
+
barSize: "medium",
|
|
135
|
+
barStyle: "classic",
|
|
136
|
+
separator: " | ",
|
|
137
|
+
maxWidth: 80,
|
|
138
|
+
clockFormat: "24h"
|
|
139
|
+
},
|
|
140
|
+
components: {
|
|
141
|
+
daily: true,
|
|
142
|
+
weekly: true,
|
|
143
|
+
monthly: true,
|
|
144
|
+
balance: true,
|
|
145
|
+
tokens: false,
|
|
146
|
+
rateLimit: false,
|
|
147
|
+
plan: false
|
|
148
|
+
},
|
|
149
|
+
colors: {
|
|
150
|
+
auto: {
|
|
151
|
+
low: "green",
|
|
152
|
+
medium: "yellow",
|
|
153
|
+
high: "red",
|
|
154
|
+
lowThreshold: 50,
|
|
155
|
+
highThreshold: 80
|
|
156
|
+
},
|
|
157
|
+
chill: {
|
|
158
|
+
low: "cyan",
|
|
159
|
+
medium: "blue",
|
|
160
|
+
high: "magenta",
|
|
161
|
+
lowThreshold: 50,
|
|
162
|
+
highThreshold: 80
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
customProviders: {},
|
|
166
|
+
pollIntervalSeconds: 30,
|
|
167
|
+
pipedRequestTimeoutMs: 800
|
|
168
|
+
};
|
|
169
|
+
var BAR_SIZE_MAP = {
|
|
170
|
+
small: 4,
|
|
171
|
+
"small-medium": 6,
|
|
172
|
+
medium: 8,
|
|
173
|
+
"medium-large": 10,
|
|
174
|
+
large: 12
|
|
175
|
+
};
|
|
176
|
+
var BUILT_IN_BAR_STYLES = {
|
|
177
|
+
classic: { fill: "━", empty: "─" },
|
|
178
|
+
block: { fill: "█", empty: "░" },
|
|
179
|
+
shade: { fill: "▓", empty: "░" },
|
|
180
|
+
pipe: { fill: "┃", empty: "┊" },
|
|
181
|
+
dot: { fill: "●", empty: "○" },
|
|
182
|
+
braille: { fill: "⣿", empty: "⣀" },
|
|
183
|
+
square: { fill: "■", empty: "□" },
|
|
184
|
+
star: { fill: "★", empty: "☆" }
|
|
185
|
+
};
|
|
186
|
+
var COMPONENT_SHORT_LABELS = {
|
|
187
|
+
daily: "D",
|
|
188
|
+
weekly: "W",
|
|
189
|
+
monthly: "M",
|
|
190
|
+
balance: "B",
|
|
191
|
+
tokens: "T",
|
|
192
|
+
rateLimit: "R",
|
|
193
|
+
plan: "P"
|
|
194
|
+
};
|
|
195
|
+
var COMPONENT_FULL_LABELS = {
|
|
196
|
+
daily: "Daily",
|
|
197
|
+
weekly: "Weekly",
|
|
198
|
+
monthly: "Monthly",
|
|
199
|
+
balance: "Balance",
|
|
200
|
+
tokens: "Tokens",
|
|
201
|
+
rateLimit: "Rate",
|
|
202
|
+
plan: "Plan"
|
|
203
|
+
};
|
|
204
|
+
// src/types/cache.ts
|
|
205
|
+
var CACHE_VERSION = 1;
|
|
206
|
+
function isCacheEntry(value) {
|
|
207
|
+
if (typeof value !== "object" || value === null)
|
|
208
|
+
return false;
|
|
209
|
+
const c = value;
|
|
210
|
+
return typeof c["version"] === "number" && typeof c["provider"] === "string" && typeof c["baseUrl"] === "string" && typeof c["tokenHash"] === "string" && typeof c["configHash"] === "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");
|
|
211
|
+
}
|
|
212
|
+
// src/services/cache.ts
|
|
213
|
+
function getCacheDir() {
|
|
214
|
+
const override = process.env["CC_API_STATUSLINE_CACHE_DIR"];
|
|
215
|
+
if (override) {
|
|
216
|
+
return override;
|
|
217
|
+
}
|
|
218
|
+
return join2(homedir2(), ".claude", "cc-api-statusline");
|
|
219
|
+
}
|
|
220
|
+
function ensureCacheDir() {
|
|
221
|
+
const dir = getCacheDir();
|
|
222
|
+
if (!existsSync2(dir)) {
|
|
223
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function getCachePath(baseUrl) {
|
|
227
|
+
const hash = shortHash(baseUrl, 12);
|
|
228
|
+
return join2(getCacheDir(), `cache-${hash}.json`);
|
|
229
|
+
}
|
|
230
|
+
function readCache(baseUrl) {
|
|
231
|
+
const path = getCachePath(baseUrl);
|
|
232
|
+
if (!existsSync2(path)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const content = readFileSync2(path, "utf-8");
|
|
237
|
+
const data = JSON.parse(content);
|
|
238
|
+
if (!isCacheEntry(data)) {
|
|
239
|
+
console.warn(`Invalid cache structure at ${path}`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return data;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.warn(`Failed to read cache from ${path}: ${error}`);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function writeCache(baseUrl, entry) {
|
|
249
|
+
const path = getCachePath(baseUrl);
|
|
250
|
+
const tmpPath = `${path}.tmp`;
|
|
251
|
+
try {
|
|
252
|
+
ensureCacheDir();
|
|
253
|
+
const content = JSON.stringify(entry, null, 2);
|
|
254
|
+
writeFileSync(tmpPath, content, { encoding: "utf-8", mode: 384 });
|
|
255
|
+
try {
|
|
256
|
+
chmodSync(tmpPath, 384);
|
|
257
|
+
} catch {}
|
|
258
|
+
renameSync(tmpPath, path);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.warn(`Failed to write cache to ${path}: ${error}`);
|
|
261
|
+
try {
|
|
262
|
+
if (existsSync2(tmpPath)) {
|
|
263
|
+
unlinkSync(tmpPath);
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function isCacheValid(entry, currentEnv) {
|
|
269
|
+
const fetchedAt = new Date(entry.fetchedAt).getTime();
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const age = now - fetchedAt;
|
|
272
|
+
const ttlMs = entry.ttlSeconds * 1000;
|
|
273
|
+
if (age >= ttlMs) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (entry.baseUrl !== currentEnv.baseUrl) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
if (entry.version !== CACHE_VERSION) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
if (entry.tokenHash !== currentEnv.tokenHash) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
function isCacheProviderValid(entry, currentProvider) {
|
|
288
|
+
return entry.provider === currentProvider;
|
|
289
|
+
}
|
|
290
|
+
function isCacheRenderedLineUsable(entry, currentConfigHash) {
|
|
291
|
+
return entry.configHash === currentConfigHash;
|
|
292
|
+
}
|
|
293
|
+
function computeConfigHash(configPath) {
|
|
294
|
+
if (!existsSync2(configPath)) {
|
|
295
|
+
return sha256("").slice(0, 12);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const bytes = readFileSync2(configPath);
|
|
299
|
+
return shortHash(bytes.toString("utf-8"), 12);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn(`Failed to read config for hash: ${error}`);
|
|
302
|
+
return sha256("").slice(0, 12);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
var DEFAULT_POLL_INTERVAL_SECONDS = 30;
|
|
306
|
+
function getEffectivePollInterval(config, envOverride) {
|
|
307
|
+
if (envOverride !== null) {
|
|
308
|
+
return Math.max(5, envOverride);
|
|
309
|
+
}
|
|
310
|
+
const fromConfig = config.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS;
|
|
311
|
+
return Math.max(5, fromConfig);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/services/config.ts
|
|
315
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
316
|
+
import { join as join3 } from "path";
|
|
317
|
+
import { homedir as homedir3 } from "os";
|
|
318
|
+
function getConfigDir() {
|
|
319
|
+
return join3(homedir3(), ".claude", "cc-api-statusline");
|
|
320
|
+
}
|
|
321
|
+
function getConfigPath(customPath) {
|
|
322
|
+
if (customPath) {
|
|
323
|
+
return customPath;
|
|
324
|
+
}
|
|
325
|
+
return join3(getConfigDir(), "config.json");
|
|
326
|
+
}
|
|
327
|
+
function deepMerge(target, source) {
|
|
328
|
+
const result = { ...target };
|
|
329
|
+
for (const key in source) {
|
|
330
|
+
const sourceValue = source[key];
|
|
331
|
+
const targetValue = result[key];
|
|
332
|
+
if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
|
|
333
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
334
|
+
} else if (sourceValue !== undefined) {
|
|
335
|
+
result[key] = sourceValue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
function validateConfig(config) {
|
|
341
|
+
if (config.display.maxWidth < 20) {
|
|
342
|
+
console.warn("Warning: display.maxWidth < 20, clamping to 20");
|
|
343
|
+
config.display.maxWidth = 20;
|
|
344
|
+
}
|
|
345
|
+
if (config.display.maxWidth > 100) {
|
|
346
|
+
console.warn("Warning: display.maxWidth > 100, clamping to 100");
|
|
347
|
+
config.display.maxWidth = 100;
|
|
348
|
+
}
|
|
349
|
+
if (config.pollIntervalSeconds !== undefined && config.pollIntervalSeconds < 5) {
|
|
350
|
+
console.warn("Warning: pollIntervalSeconds < 5, clamping to 5");
|
|
351
|
+
config.pollIntervalSeconds = 5;
|
|
352
|
+
}
|
|
353
|
+
if (config.pipedRequestTimeoutMs !== undefined && config.pipedRequestTimeoutMs < 100) {
|
|
354
|
+
console.warn("Warning: pipedRequestTimeoutMs < 100, clamping to 100");
|
|
355
|
+
config.pipedRequestTimeoutMs = 100;
|
|
356
|
+
}
|
|
357
|
+
return config;
|
|
358
|
+
}
|
|
359
|
+
function loadConfig(configPath) {
|
|
360
|
+
const path = getConfigPath(configPath);
|
|
361
|
+
if (!existsSync3(path)) {
|
|
362
|
+
return DEFAULT_CONFIG;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const content = readFileSync3(path, "utf-8");
|
|
366
|
+
const userConfig = JSON.parse(content);
|
|
367
|
+
const merged = deepMerge(DEFAULT_CONFIG, userConfig);
|
|
368
|
+
return validateConfig(merged);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.warn(`Warning: Could not load config from ${path}: ${error}`);
|
|
371
|
+
console.warn("Using default configuration");
|
|
372
|
+
return DEFAULT_CONFIG;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/providers/http.ts
|
|
377
|
+
class HttpError extends Error {
|
|
378
|
+
statusCode;
|
|
379
|
+
response;
|
|
380
|
+
constructor(message, statusCode, response) {
|
|
381
|
+
super(message);
|
|
382
|
+
this.statusCode = statusCode;
|
|
383
|
+
this.response = response;
|
|
384
|
+
this.name = "HttpError";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
class TimeoutError extends Error {
|
|
389
|
+
constructor(message = "Request timed out") {
|
|
390
|
+
super(message);
|
|
391
|
+
this.name = "TimeoutError";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
class RedirectError extends Error {
|
|
396
|
+
constructor(message) {
|
|
397
|
+
super(message);
|
|
398
|
+
this.name = "RedirectError";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class ResponseTooLargeError extends Error {
|
|
403
|
+
constructor(message = "Response body exceeds 1MB limit") {
|
|
404
|
+
super(message);
|
|
405
|
+
this.name = "ResponseTooLargeError";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function isSecureUrl(url) {
|
|
409
|
+
try {
|
|
410
|
+
const parsed = new URL(url);
|
|
411
|
+
if (parsed.protocol === "https:") {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
if (parsed.protocol === "http:") {
|
|
415
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
416
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function getHostname(url) {
|
|
426
|
+
try {
|
|
427
|
+
return new URL(url).hostname.toLowerCase();
|
|
428
|
+
} catch {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function readBodyWithLimit(response) {
|
|
433
|
+
const MAX_SIZE = 1024 * 1024;
|
|
434
|
+
let bytesRead = 0;
|
|
435
|
+
const chunks = [];
|
|
436
|
+
if (!response.body) {
|
|
437
|
+
return "";
|
|
438
|
+
}
|
|
439
|
+
const reader = response.body.getReader();
|
|
440
|
+
const decoder = new TextDecoder;
|
|
441
|
+
try {
|
|
442
|
+
while (true) {
|
|
443
|
+
const result2 = await reader.read();
|
|
444
|
+
if (result2.done)
|
|
445
|
+
break;
|
|
446
|
+
if (result2.value) {
|
|
447
|
+
const chunk = result2.value;
|
|
448
|
+
bytesRead += chunk.length;
|
|
449
|
+
if (bytesRead > MAX_SIZE) {
|
|
450
|
+
await reader.cancel();
|
|
451
|
+
throw new ResponseTooLargeError(`Response body exceeds 1MB limit (read ${bytesRead} bytes)`);
|
|
452
|
+
}
|
|
453
|
+
chunks.push(chunk);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
457
|
+
const result = new Uint8Array(totalLength);
|
|
458
|
+
let offset = 0;
|
|
459
|
+
for (const chunk of chunks) {
|
|
460
|
+
result.set(chunk, offset);
|
|
461
|
+
offset += chunk.length;
|
|
462
|
+
}
|
|
463
|
+
return decoder.decode(result);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error instanceof ResponseTooLargeError) {
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
throw new HttpError(`Failed to read response body: ${error}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function secureFetch(url, options = {}, timeoutMs = 5000, userAgent) {
|
|
472
|
+
if (!isSecureUrl(url)) {
|
|
473
|
+
throw new HttpError(`Insecure URL rejected (must be HTTPS or localhost): ${url}`);
|
|
474
|
+
}
|
|
475
|
+
const originalHostname = getHostname(url);
|
|
476
|
+
if (!originalHostname) {
|
|
477
|
+
throw new HttpError(`Invalid URL: ${url}`);
|
|
478
|
+
}
|
|
479
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
480
|
+
const fetchOptions = {
|
|
481
|
+
...options,
|
|
482
|
+
redirect: "manual",
|
|
483
|
+
signal
|
|
484
|
+
};
|
|
485
|
+
if (userAgent) {
|
|
486
|
+
const headers = new Headers(options.headers);
|
|
487
|
+
headers.set("User-Agent", userAgent);
|
|
488
|
+
fetchOptions.headers = headers;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const response = await fetch(url, fetchOptions);
|
|
492
|
+
if (response.status >= 300 && response.status < 400) {
|
|
493
|
+
const location = response.headers.get("Location");
|
|
494
|
+
if (location) {
|
|
495
|
+
const redirectHostname = getHostname(location);
|
|
496
|
+
if (redirectHostname && redirectHostname !== originalHostname) {
|
|
497
|
+
throw new RedirectError(`Cross-domain redirect blocked: ${originalHostname} → ${redirectHostname}`);
|
|
498
|
+
}
|
|
499
|
+
throw new RedirectError(`Redirect detected to: ${location}`);
|
|
500
|
+
}
|
|
501
|
+
throw new RedirectError("Redirect detected but Location header missing");
|
|
502
|
+
}
|
|
503
|
+
if (!response.ok) {
|
|
504
|
+
let errorContext = response.statusText;
|
|
505
|
+
try {
|
|
506
|
+
const errorBody = await readBodyWithLimit(response);
|
|
507
|
+
if (errorBody)
|
|
508
|
+
errorContext = errorBody.slice(0, 200);
|
|
509
|
+
} catch {}
|
|
510
|
+
throw new HttpError(`HTTP ${response.status}: ${errorContext}`, response.status, response);
|
|
511
|
+
}
|
|
512
|
+
return await readBodyWithLimit(response);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error instanceof Error) {
|
|
515
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") {
|
|
516
|
+
throw new TimeoutError(`Request timed out after ${timeoutMs}ms`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (error instanceof HttpError || error instanceof TimeoutError || error instanceof RedirectError || error instanceof ResponseTooLargeError) {
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
if (error instanceof Error && error.message.includes("timed out")) {
|
|
523
|
+
throw new TimeoutError(`Request timed out after ${timeoutMs}ms`);
|
|
524
|
+
}
|
|
525
|
+
throw new HttpError(`Network error: ${error}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/services/user-agent.ts
|
|
530
|
+
import { execSync } from "child_process";
|
|
531
|
+
import { join as join5 } from "path";
|
|
532
|
+
import { homedir as homedir5 } from "os";
|
|
533
|
+
|
|
534
|
+
// src/services/logger.ts
|
|
535
|
+
import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
536
|
+
import { join as join4, dirname } from "path";
|
|
537
|
+
import { homedir as homedir4 } from "os";
|
|
538
|
+
|
|
539
|
+
class Logger {
|
|
540
|
+
enabled;
|
|
541
|
+
logPath;
|
|
542
|
+
constructor() {
|
|
543
|
+
this.enabled = !!(process.env["DEBUG"] || process.env["CC_STATUSLINE_DEBUG"]);
|
|
544
|
+
const logDir = process.env["CC_API_STATUSLINE_LOG_DIR"] || join4(homedir4(), ".claude", "cc-api-statusline");
|
|
545
|
+
this.logPath = join4(logDir, "debug.log");
|
|
546
|
+
if (this.enabled) {
|
|
547
|
+
this.ensureLogDir();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
ensureLogDir() {
|
|
551
|
+
try {
|
|
552
|
+
const dir = dirname(this.logPath);
|
|
553
|
+
if (!existsSync4(dir)) {
|
|
554
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
this.enabled = false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
format(level, message, data) {
|
|
561
|
+
const timestamp = new Date().toISOString();
|
|
562
|
+
const dataStr = data ? ` ${JSON.stringify(data)}` : "";
|
|
563
|
+
return `[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
write(level, message, data) {
|
|
567
|
+
if (!this.enabled) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const entry = this.format(level, message, data);
|
|
572
|
+
appendFileSync(this.logPath, entry, { encoding: "utf-8" });
|
|
573
|
+
} catch {}
|
|
574
|
+
}
|
|
575
|
+
debug(message, data) {
|
|
576
|
+
this.write("debug", message, data);
|
|
577
|
+
}
|
|
578
|
+
info(message, data) {
|
|
579
|
+
this.write("info", message, data);
|
|
580
|
+
}
|
|
581
|
+
warn(message, data) {
|
|
582
|
+
this.write("warn", message, data);
|
|
583
|
+
}
|
|
584
|
+
error(message, data) {
|
|
585
|
+
this.write("error", message, data);
|
|
586
|
+
}
|
|
587
|
+
isEnabled() {
|
|
588
|
+
return this.enabled;
|
|
589
|
+
}
|
|
590
|
+
getLogPath() {
|
|
591
|
+
return this.logPath;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
var logger = new Logger;
|
|
595
|
+
|
|
596
|
+
// src/services/user-agent.ts
|
|
597
|
+
var FALLBACK_UA = "claude-cli/2.1.56 (external, cli)";
|
|
598
|
+
function resolveUserAgent(config) {
|
|
599
|
+
if (!config) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
if (typeof config === "string") {
|
|
603
|
+
return config || null;
|
|
604
|
+
}
|
|
605
|
+
return detectClaudeCodeUA();
|
|
606
|
+
}
|
|
607
|
+
function detectClaudeCodeUA() {
|
|
608
|
+
logger.debug("UA spoofing enabled, attempting detection");
|
|
609
|
+
const version = detectClaudeVersion();
|
|
610
|
+
if (version) {
|
|
611
|
+
const ua = `claude-cli/${version} (external, cli)`;
|
|
612
|
+
logger.debug(`Detected Claude Code version: ${version}`);
|
|
613
|
+
return ua;
|
|
614
|
+
}
|
|
615
|
+
logger.debug(`Detection failed, using fallback: ${FALLBACK_UA}`);
|
|
616
|
+
return FALLBACK_UA;
|
|
617
|
+
}
|
|
618
|
+
function detectClaudeVersion() {
|
|
619
|
+
try {
|
|
620
|
+
if (!process.env["CLAUDECODE"]) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const claudePath = join5(homedir5(), ".claude", "bin", "claude");
|
|
624
|
+
const result = execSync(`"${claudePath}" --version`, {
|
|
625
|
+
encoding: "utf-8",
|
|
626
|
+
timeout: 1000,
|
|
627
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
628
|
+
});
|
|
629
|
+
const match = result.match(/(\d+\.\d+\.\d+)/);
|
|
630
|
+
return match ? match[1] : null;
|
|
631
|
+
} catch {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/providers/sub2api.ts
|
|
637
|
+
function computeNextMidnightUTC() {
|
|
638
|
+
const now = new Date;
|
|
639
|
+
const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0, 0));
|
|
640
|
+
return tomorrow.toISOString();
|
|
641
|
+
}
|
|
642
|
+
function computeNextMondayUTC() {
|
|
643
|
+
const now = new Date;
|
|
644
|
+
const dayOfWeek = now.getUTCDay();
|
|
645
|
+
const daysUntilMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek;
|
|
646
|
+
const nextMonday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilMonday, 0, 0, 0, 0));
|
|
647
|
+
return nextMonday.toISOString();
|
|
648
|
+
}
|
|
649
|
+
function computeFirstOfNextMonthUTC() {
|
|
650
|
+
const now = new Date;
|
|
651
|
+
const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0));
|
|
652
|
+
return nextMonth.toISOString();
|
|
653
|
+
}
|
|
654
|
+
function mapPeriodTokens(data) {
|
|
655
|
+
if (!data)
|
|
656
|
+
return null;
|
|
657
|
+
return {
|
|
658
|
+
requests: data.requests ?? 0,
|
|
659
|
+
inputTokens: data.input_tokens ?? 0,
|
|
660
|
+
outputTokens: data.output_tokens ?? 0,
|
|
661
|
+
cacheCreationTokens: data.cache_creation_tokens ?? 0,
|
|
662
|
+
cacheReadTokens: data.cache_read_tokens ?? 0,
|
|
663
|
+
totalTokens: data.total_tokens ?? 0,
|
|
664
|
+
cost: data.cost ?? 0
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function createQuotaWindow(used, limit, resetsAt) {
|
|
668
|
+
if (used === undefined)
|
|
669
|
+
return null;
|
|
670
|
+
const actualLimit = limit === null || limit === undefined ? null : limit;
|
|
671
|
+
let remaining = null;
|
|
672
|
+
if (actualLimit !== null) {
|
|
673
|
+
remaining = Math.max(0, actualLimit - used);
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
used,
|
|
677
|
+
limit: actualLimit,
|
|
678
|
+
remaining,
|
|
679
|
+
resetsAt
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function fetchSub2api(baseUrl, token, config, timeoutMs = 5000) {
|
|
683
|
+
const url = `${baseUrl}/v1/usage`;
|
|
684
|
+
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
685
|
+
if (resolvedUA) {
|
|
686
|
+
logger.debug(`Using User-Agent: ${resolvedUA}`);
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
const responseText = await secureFetch(url, {
|
|
690
|
+
method: "GET",
|
|
691
|
+
headers: {
|
|
692
|
+
Authorization: `Bearer ${token}`,
|
|
693
|
+
Accept: "application/json"
|
|
694
|
+
}
|
|
695
|
+
}, timeoutMs, resolvedUA);
|
|
696
|
+
const data = JSON.parse(responseText);
|
|
697
|
+
const hasSubscription = !!data.subscription;
|
|
698
|
+
const billingMode = hasSubscription ? "subscription" : "balance";
|
|
699
|
+
const result = {
|
|
700
|
+
provider: "sub2api",
|
|
701
|
+
billingMode,
|
|
702
|
+
planName: data.planName ?? "Unknown",
|
|
703
|
+
fetchedAt: new Date().toISOString(),
|
|
704
|
+
resetSemantics: "end-of-day",
|
|
705
|
+
daily: null,
|
|
706
|
+
weekly: null,
|
|
707
|
+
monthly: null,
|
|
708
|
+
balance: null,
|
|
709
|
+
resetsAt: null,
|
|
710
|
+
tokenStats: null,
|
|
711
|
+
rateLimit: null
|
|
712
|
+
};
|
|
713
|
+
if (billingMode === "balance") {
|
|
714
|
+
const remaining = data.remaining ?? 0;
|
|
715
|
+
if (remaining === -1) {
|
|
716
|
+
result.balance = {
|
|
717
|
+
remaining: -1,
|
|
718
|
+
initial: null,
|
|
719
|
+
unit: data.unit ?? "USD"
|
|
720
|
+
};
|
|
721
|
+
} else {
|
|
722
|
+
result.balance = {
|
|
723
|
+
remaining,
|
|
724
|
+
initial: null,
|
|
725
|
+
unit: data.unit ?? "USD"
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
const sub = data.subscription;
|
|
730
|
+
if (!sub) {
|
|
731
|
+
throw new Error("Subscription mode but no subscription object in response");
|
|
732
|
+
}
|
|
733
|
+
result.daily = createQuotaWindow(sub.daily_usage_usd, sub.daily_limit_usd, computeNextMidnightUTC());
|
|
734
|
+
result.weekly = createQuotaWindow(sub.weekly_usage_usd, sub.weekly_limit_usd, computeNextMondayUTC());
|
|
735
|
+
result.monthly = createQuotaWindow(sub.monthly_usage_usd, sub.monthly_limit_usd, computeFirstOfNextMonthUTC());
|
|
736
|
+
result.resetsAt = computeSoonestReset(result);
|
|
737
|
+
}
|
|
738
|
+
if (data.usage) {
|
|
739
|
+
result.tokenStats = {
|
|
740
|
+
today: mapPeriodTokens(data.usage.today),
|
|
741
|
+
total: mapPeriodTokens(data.usage.total),
|
|
742
|
+
rpm: data.usage.rpm ?? null,
|
|
743
|
+
tpm: data.usage.tpm ?? null
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return result;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
if (error instanceof HttpError && error.statusCode === 429) {
|
|
749
|
+
return {
|
|
750
|
+
provider: "sub2api",
|
|
751
|
+
billingMode: "subscription",
|
|
752
|
+
planName: "Quota Exhausted",
|
|
753
|
+
fetchedAt: new Date().toISOString(),
|
|
754
|
+
resetSemantics: "end-of-day",
|
|
755
|
+
daily: {
|
|
756
|
+
used: 0,
|
|
757
|
+
limit: 0,
|
|
758
|
+
remaining: 0,
|
|
759
|
+
resetsAt: computeNextMidnightUTC()
|
|
760
|
+
},
|
|
761
|
+
weekly: null,
|
|
762
|
+
monthly: null,
|
|
763
|
+
balance: null,
|
|
764
|
+
resetsAt: computeNextMidnightUTC(),
|
|
765
|
+
tokenStats: null,
|
|
766
|
+
rateLimit: null
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/providers/claude-relay-service.ts
|
|
774
|
+
function computeWeeklyResetTime(resetDay, resetHour) {
|
|
775
|
+
const now = new Date;
|
|
776
|
+
const currentDay = now.getUTCDay();
|
|
777
|
+
const currentHour = now.getUTCHours();
|
|
778
|
+
let daysUntilReset = resetDay - currentDay;
|
|
779
|
+
if (daysUntilReset < 0 || daysUntilReset === 0 && currentHour >= resetHour) {
|
|
780
|
+
daysUntilReset += 7;
|
|
781
|
+
}
|
|
782
|
+
const resetDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilReset, resetHour, 0, 0, 0));
|
|
783
|
+
return resetDate.toISOString();
|
|
784
|
+
}
|
|
785
|
+
function createQuotaWindow2(used, limit, resetsAt) {
|
|
786
|
+
if (used === undefined)
|
|
787
|
+
return null;
|
|
788
|
+
const actualLimit = limit && limit > 0 ? limit : null;
|
|
789
|
+
let remaining = null;
|
|
790
|
+
if (actualLimit !== null) {
|
|
791
|
+
remaining = Math.max(0, actualLimit - used);
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
used,
|
|
795
|
+
limit: actualLimit,
|
|
796
|
+
remaining,
|
|
797
|
+
resetsAt
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000) {
|
|
801
|
+
const url = `${baseUrl}/apiStats/api/user-stats`;
|
|
802
|
+
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
803
|
+
if (resolvedUA) {
|
|
804
|
+
logger.debug(`Using User-Agent: ${resolvedUA}`);
|
|
805
|
+
}
|
|
806
|
+
const responseText = await secureFetch(url, {
|
|
807
|
+
method: "POST",
|
|
808
|
+
headers: {
|
|
809
|
+
"Content-Type": "application/json",
|
|
810
|
+
Accept: "application/json"
|
|
811
|
+
},
|
|
812
|
+
body: JSON.stringify({ apiKey: token })
|
|
813
|
+
}, timeoutMs, resolvedUA);
|
|
814
|
+
const response = JSON.parse(responseText);
|
|
815
|
+
if (!response.success) {
|
|
816
|
+
throw new HttpError("Relay API returned success: false");
|
|
817
|
+
}
|
|
818
|
+
const data = response.data;
|
|
819
|
+
const limits = data.limits;
|
|
820
|
+
const result = {
|
|
821
|
+
provider: "claude-relay-service",
|
|
822
|
+
billingMode: "subscription",
|
|
823
|
+
planName: data.name ?? "API Key",
|
|
824
|
+
fetchedAt: new Date().toISOString(),
|
|
825
|
+
resetSemantics: "rolling-window",
|
|
826
|
+
daily: null,
|
|
827
|
+
weekly: null,
|
|
828
|
+
monthly: null,
|
|
829
|
+
balance: null,
|
|
830
|
+
resetsAt: null,
|
|
831
|
+
tokenStats: null,
|
|
832
|
+
rateLimit: null
|
|
833
|
+
};
|
|
834
|
+
result.daily = createQuotaWindow2(limits.currentDailyCost, limits.dailyCostLimit, null);
|
|
835
|
+
if (limits.weeklyResetDay !== undefined && limits.weeklyResetHour !== undefined) {
|
|
836
|
+
const weeklyResetsAt = computeWeeklyResetTime(limits.weeklyResetDay, limits.weeklyResetHour);
|
|
837
|
+
result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, weeklyResetsAt);
|
|
838
|
+
} else {
|
|
839
|
+
result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, null);
|
|
840
|
+
}
|
|
841
|
+
result.monthly = null;
|
|
842
|
+
if (limits.windowEndTime) {
|
|
843
|
+
result.resetsAt = new Date(limits.windowEndTime).toISOString();
|
|
844
|
+
}
|
|
845
|
+
if (data.usage?.total) {
|
|
846
|
+
const total = data.usage.total;
|
|
847
|
+
result.tokenStats = {
|
|
848
|
+
today: null,
|
|
849
|
+
total: {
|
|
850
|
+
requests: total.requests ?? 0,
|
|
851
|
+
inputTokens: total.inputTokens ?? 0,
|
|
852
|
+
outputTokens: total.outputTokens ?? 0,
|
|
853
|
+
cacheCreationTokens: total.cacheCreateTokens ?? 0,
|
|
854
|
+
cacheReadTokens: total.cacheReadTokens ?? 0,
|
|
855
|
+
totalTokens: total.tokens ?? (total.inputTokens ?? 0) + (total.outputTokens ?? 0),
|
|
856
|
+
cost: total.cost ?? 0
|
|
857
|
+
},
|
|
858
|
+
rpm: null,
|
|
859
|
+
tpm: null
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (limits.rateLimitWindow !== undefined) {
|
|
863
|
+
result.rateLimit = {
|
|
864
|
+
windowSeconds: limits.rateLimitWindow * 60,
|
|
865
|
+
requestsUsed: limits.currentWindowRequests ?? 0,
|
|
866
|
+
requestsLimit: limits.rateLimitRequests && limits.rateLimitRequests > 0 ? limits.rateLimitRequests : null,
|
|
867
|
+
costUsed: limits.currentWindowCost ?? 0,
|
|
868
|
+
costLimit: limits.rateLimitCost && limits.rateLimitCost > 0 ? limits.rateLimitCost : null,
|
|
869
|
+
remainingSeconds: limits.windowRemainingSeconds ?? 0
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/providers/custom.ts
|
|
876
|
+
function resolveJsonPath(data, path) {
|
|
877
|
+
if (!path.startsWith("$.")) {
|
|
878
|
+
return path;
|
|
879
|
+
}
|
|
880
|
+
const parts = path.slice(2).split(/\.|\[|\]/).filter((p) => p.length > 0);
|
|
881
|
+
let current = data;
|
|
882
|
+
for (const part of parts) {
|
|
883
|
+
if (current === null || current === undefined) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
if (typeof current !== "object") {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
const index = parseInt(part, 10);
|
|
890
|
+
if (!isNaN(index) && Array.isArray(current)) {
|
|
891
|
+
current = current[index];
|
|
892
|
+
} else {
|
|
893
|
+
current = current[part];
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return current;
|
|
897
|
+
}
|
|
898
|
+
function extractValue(data, mapping, defaultValue = null) {
|
|
899
|
+
if (!mapping)
|
|
900
|
+
return defaultValue;
|
|
901
|
+
const resolved = resolveJsonPath(data, mapping);
|
|
902
|
+
return resolved ?? defaultValue;
|
|
903
|
+
}
|
|
904
|
+
function extractNumber(data, mapping) {
|
|
905
|
+
const value = extractValue(data, mapping);
|
|
906
|
+
if (typeof value === "number")
|
|
907
|
+
return value;
|
|
908
|
+
if (typeof value === "string") {
|
|
909
|
+
const parsed = parseFloat(value);
|
|
910
|
+
return isNaN(parsed) ? null : parsed;
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
function extractString(data, mapping, defaultValue = "") {
|
|
915
|
+
const value = extractValue(data, mapping, defaultValue);
|
|
916
|
+
if (typeof value === "string")
|
|
917
|
+
return value;
|
|
918
|
+
return String(value);
|
|
919
|
+
}
|
|
920
|
+
function validateCustomProvider(providerConfig) {
|
|
921
|
+
if (!providerConfig.id)
|
|
922
|
+
return "Custom provider missing required field: id";
|
|
923
|
+
if (!providerConfig.endpoint)
|
|
924
|
+
return "Custom provider missing required field: endpoint";
|
|
925
|
+
if (!providerConfig.method)
|
|
926
|
+
return "Custom provider missing required field: method";
|
|
927
|
+
if (!providerConfig.auth)
|
|
928
|
+
return "Custom provider missing required field: auth";
|
|
929
|
+
if (!providerConfig.responseMapping)
|
|
930
|
+
return "Custom provider missing required field: responseMapping";
|
|
931
|
+
if (!providerConfig.endpoint.startsWith("/")) {
|
|
932
|
+
return "Custom provider endpoint must start with /";
|
|
933
|
+
}
|
|
934
|
+
if (!providerConfig.responseMapping.billingMode) {
|
|
935
|
+
return "Custom provider responseMapping must include billingMode";
|
|
936
|
+
}
|
|
937
|
+
if (providerConfig.auth.type === "header" && !providerConfig.auth.header) {
|
|
938
|
+
return 'Custom provider auth.type="header" requires auth.header';
|
|
939
|
+
}
|
|
940
|
+
if (providerConfig.auth.type === "body" && !providerConfig.auth.bodyField) {
|
|
941
|
+
return 'Custom provider auth.type="body" requires auth.bodyField';
|
|
942
|
+
}
|
|
943
|
+
if (providerConfig.urlPatterns && !Array.isArray(providerConfig.urlPatterns)) {
|
|
944
|
+
return "Custom provider urlPatterns must be an array";
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs = 5000) {
|
|
949
|
+
const validationError = validateCustomProvider(providerConfig);
|
|
950
|
+
if (validationError) {
|
|
951
|
+
throw new Error(`Invalid custom provider providerConfig: ${validationError}`);
|
|
952
|
+
}
|
|
953
|
+
const url = `${baseUrl}${providerConfig.endpoint}`;
|
|
954
|
+
const headers = {
|
|
955
|
+
Accept: "application/json"
|
|
956
|
+
};
|
|
957
|
+
if (providerConfig.contentType) {
|
|
958
|
+
headers["Content-Type"] = providerConfig.contentType;
|
|
959
|
+
}
|
|
960
|
+
if (providerConfig.auth.type === "header" && providerConfig.auth.header) {
|
|
961
|
+
const prefix = providerConfig.auth.prefix ?? "";
|
|
962
|
+
headers[providerConfig.auth.header] = `${prefix}${token}`;
|
|
963
|
+
}
|
|
964
|
+
let body;
|
|
965
|
+
if (providerConfig.method === "POST") {
|
|
966
|
+
if (providerConfig.auth.type === "body" && providerConfig.auth.bodyField) {
|
|
967
|
+
const bodyObj = { ...providerConfig.requestBody };
|
|
968
|
+
bodyObj[providerConfig.auth.bodyField] = token;
|
|
969
|
+
body = JSON.stringify(bodyObj);
|
|
970
|
+
} else if (providerConfig.requestBody) {
|
|
971
|
+
body = JSON.stringify(providerConfig.requestBody);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const providerUA = providerConfig.spoofClaudeCodeUA;
|
|
975
|
+
const globalUA = appConfig.spoofClaudeCodeUA;
|
|
976
|
+
const effectiveUA = providerUA !== undefined ? providerUA : globalUA;
|
|
977
|
+
const resolvedUA = resolveUserAgent(effectiveUA);
|
|
978
|
+
if (resolvedUA) {
|
|
979
|
+
logger.debug(`Using User-Agent for ${providerConfig.id}: ${resolvedUA}`);
|
|
980
|
+
}
|
|
981
|
+
const responseText = await secureFetch(url, {
|
|
982
|
+
method: providerConfig.method,
|
|
983
|
+
headers,
|
|
984
|
+
body
|
|
985
|
+
}, timeoutMs, resolvedUA);
|
|
986
|
+
const responseData = JSON.parse(responseText);
|
|
987
|
+
const mapping = providerConfig.responseMapping;
|
|
988
|
+
const billingModeStr = extractString(responseData, mapping.billingMode, "subscription");
|
|
989
|
+
const billingMode = billingModeStr === "balance" ? "balance" : "subscription";
|
|
990
|
+
const planName = extractString(responseData, mapping.planName, providerConfig.displayName ?? providerConfig.id);
|
|
991
|
+
const result = createEmptyNormalizedUsage(providerConfig.id, billingMode, planName);
|
|
992
|
+
result.resetSemantics = billingMode === "balance" ? "expiry" : "end-of-day";
|
|
993
|
+
if (mapping["balance.remaining"]) {
|
|
994
|
+
const remaining = extractNumber(responseData, mapping["balance.remaining"]);
|
|
995
|
+
if (remaining !== null) {
|
|
996
|
+
result.balance = {
|
|
997
|
+
remaining,
|
|
998
|
+
initial: extractNumber(responseData, mapping["balance.initial"]),
|
|
999
|
+
unit: extractString(responseData, mapping["balance.unit"], "USD")
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const dailyUsed = extractNumber(responseData, mapping["daily.used"]);
|
|
1004
|
+
const dailyLimitRaw = extractNumber(responseData, mapping["daily.limit"]);
|
|
1005
|
+
const dailyLimit = dailyLimitRaw === 0 ? null : dailyLimitRaw;
|
|
1006
|
+
if (dailyUsed !== null) {
|
|
1007
|
+
result.daily = {
|
|
1008
|
+
used: dailyUsed,
|
|
1009
|
+
limit: dailyLimit,
|
|
1010
|
+
remaining: dailyLimit !== null ? Math.max(0, dailyLimit - dailyUsed) : null,
|
|
1011
|
+
resetsAt: extractString(responseData, mapping["daily.resetsAt"], "") || null
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const weeklyUsed = extractNumber(responseData, mapping["weekly.used"]);
|
|
1015
|
+
const weeklyLimitRaw = extractNumber(responseData, mapping["weekly.limit"]);
|
|
1016
|
+
const weeklyLimit = weeklyLimitRaw === 0 ? null : weeklyLimitRaw;
|
|
1017
|
+
if (weeklyUsed !== null) {
|
|
1018
|
+
result.weekly = {
|
|
1019
|
+
used: weeklyUsed,
|
|
1020
|
+
limit: weeklyLimit,
|
|
1021
|
+
remaining: weeklyLimit !== null ? Math.max(0, weeklyLimit - weeklyUsed) : null,
|
|
1022
|
+
resetsAt: extractString(responseData, mapping["weekly.resetsAt"], "") || null
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const monthlyUsed = extractNumber(responseData, mapping["monthly.used"]);
|
|
1026
|
+
const monthlyLimitRaw = extractNumber(responseData, mapping["monthly.limit"]);
|
|
1027
|
+
const monthlyLimit = monthlyLimitRaw === 0 ? null : monthlyLimitRaw;
|
|
1028
|
+
if (monthlyUsed !== null) {
|
|
1029
|
+
result.monthly = {
|
|
1030
|
+
used: monthlyUsed,
|
|
1031
|
+
limit: monthlyLimit,
|
|
1032
|
+
remaining: monthlyLimit !== null ? Math.max(0, monthlyLimit - monthlyUsed) : null,
|
|
1033
|
+
resetsAt: extractString(responseData, mapping["monthly.resetsAt"], "") || null
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (mapping["tokenStats.today.requests"]) {
|
|
1037
|
+
const todayRequests = extractNumber(responseData, mapping["tokenStats.today.requests"]);
|
|
1038
|
+
if (todayRequests !== null) {
|
|
1039
|
+
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1040
|
+
result.tokenStats.today = {
|
|
1041
|
+
requests: todayRequests,
|
|
1042
|
+
inputTokens: extractNumber(responseData, mapping["tokenStats.today.inputTokens"]) ?? 0,
|
|
1043
|
+
outputTokens: extractNumber(responseData, mapping["tokenStats.today.outputTokens"]) ?? 0,
|
|
1044
|
+
cacheCreationTokens: extractNumber(responseData, mapping["tokenStats.today.cacheCreationTokens"]) ?? 0,
|
|
1045
|
+
cacheReadTokens: extractNumber(responseData, mapping["tokenStats.today.cacheReadTokens"]) ?? 0,
|
|
1046
|
+
totalTokens: extractNumber(responseData, mapping["tokenStats.today.totalTokens"]) ?? 0,
|
|
1047
|
+
cost: extractNumber(responseData, mapping["tokenStats.today.cost"]) ?? 0
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (mapping["tokenStats.total.requests"]) {
|
|
1052
|
+
const totalRequests = extractNumber(responseData, mapping["tokenStats.total.requests"]);
|
|
1053
|
+
if (totalRequests !== null) {
|
|
1054
|
+
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1055
|
+
result.tokenStats.total = {
|
|
1056
|
+
requests: totalRequests,
|
|
1057
|
+
inputTokens: extractNumber(responseData, mapping["tokenStats.total.inputTokens"]) ?? 0,
|
|
1058
|
+
outputTokens: extractNumber(responseData, mapping["tokenStats.total.outputTokens"]) ?? 0,
|
|
1059
|
+
cacheCreationTokens: extractNumber(responseData, mapping["tokenStats.total.cacheCreationTokens"]) ?? 0,
|
|
1060
|
+
cacheReadTokens: extractNumber(responseData, mapping["tokenStats.total.cacheReadTokens"]) ?? 0,
|
|
1061
|
+
totalTokens: extractNumber(responseData, mapping["tokenStats.total.totalTokens"]) ?? 0,
|
|
1062
|
+
cost: extractNumber(responseData, mapping["tokenStats.total.cost"]) ?? 0
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (mapping["tokenStats.rpm"]) {
|
|
1067
|
+
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1068
|
+
result.tokenStats.rpm = extractNumber(responseData, mapping["tokenStats.rpm"]);
|
|
1069
|
+
}
|
|
1070
|
+
if (mapping["tokenStats.tpm"]) {
|
|
1071
|
+
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1072
|
+
result.tokenStats.tpm = extractNumber(responseData, mapping["tokenStats.tpm"]);
|
|
1073
|
+
}
|
|
1074
|
+
if (mapping["rateLimit.windowSeconds"]) {
|
|
1075
|
+
const windowSeconds = extractNumber(responseData, mapping["rateLimit.windowSeconds"]);
|
|
1076
|
+
if (windowSeconds !== null) {
|
|
1077
|
+
result.rateLimit = {
|
|
1078
|
+
windowSeconds,
|
|
1079
|
+
requestsUsed: extractNumber(responseData, mapping["rateLimit.requestsUsed"]) ?? 0,
|
|
1080
|
+
requestsLimit: extractNumber(responseData, mapping["rateLimit.requestsLimit"]),
|
|
1081
|
+
costUsed: extractNumber(responseData, mapping["rateLimit.costUsed"]) ?? 0,
|
|
1082
|
+
costLimit: extractNumber(responseData, mapping["rateLimit.costLimit"]),
|
|
1083
|
+
remainingSeconds: extractNumber(responseData, mapping["rateLimit.remainingSeconds"]) ?? 0
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (result.daily?.resetsAt || result.weekly?.resetsAt || result.monthly?.resetsAt) {
|
|
1088
|
+
const times = [];
|
|
1089
|
+
if (result.daily?.resetsAt)
|
|
1090
|
+
times.push(result.daily.resetsAt);
|
|
1091
|
+
if (result.weekly?.resetsAt)
|
|
1092
|
+
times.push(result.weekly.resetsAt);
|
|
1093
|
+
if (result.monthly?.resetsAt)
|
|
1094
|
+
times.push(result.monthly.resetsAt);
|
|
1095
|
+
times.sort();
|
|
1096
|
+
result.resetsAt = times[0] ?? null;
|
|
1097
|
+
}
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/providers/autodetect.ts
|
|
1102
|
+
var detectionCache = new Map;
|
|
1103
|
+
function detectProvider(baseUrl, customProviders = {}) {
|
|
1104
|
+
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1105
|
+
for (const [providerId, config] of Object.entries(customProviders)) {
|
|
1106
|
+
if (config.urlPatterns && config.urlPatterns.length > 0) {
|
|
1107
|
+
for (const pattern of config.urlPatterns) {
|
|
1108
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
1109
|
+
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1110
|
+
return providerId;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (normalizedUrl.includes("/apistats") || normalizedUrl.includes("relay") || normalizedUrl.includes("/api/user-stats")) {
|
|
1116
|
+
return "claude-relay-service";
|
|
1117
|
+
}
|
|
1118
|
+
return "sub2api";
|
|
1119
|
+
}
|
|
1120
|
+
function resolveProvider(baseUrl, providerOverride, customProviders = {}) {
|
|
1121
|
+
if (providerOverride) {
|
|
1122
|
+
return providerOverride;
|
|
1123
|
+
}
|
|
1124
|
+
const cached = detectionCache.get(baseUrl);
|
|
1125
|
+
if (cached) {
|
|
1126
|
+
return cached.provider;
|
|
1127
|
+
}
|
|
1128
|
+
const provider = detectProvider(baseUrl, customProviders);
|
|
1129
|
+
detectionCache.set(baseUrl, {
|
|
1130
|
+
provider,
|
|
1131
|
+
detectedAt: new Date().toISOString()
|
|
1132
|
+
});
|
|
1133
|
+
return provider;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/providers/index.ts
|
|
1137
|
+
var BUILT_IN_ADAPTERS = {
|
|
1138
|
+
sub2api: {
|
|
1139
|
+
fetch: fetchSub2api
|
|
1140
|
+
},
|
|
1141
|
+
"claude-relay-service": {
|
|
1142
|
+
fetch: fetchClaudeRelayService
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
function getProvider(providerId, customProviders = {}) {
|
|
1146
|
+
if (BUILT_IN_ADAPTERS[providerId]) {
|
|
1147
|
+
return BUILT_IN_ADAPTERS[providerId];
|
|
1148
|
+
}
|
|
1149
|
+
const customConfig = customProviders[providerId];
|
|
1150
|
+
if (customConfig) {
|
|
1151
|
+
return {
|
|
1152
|
+
fetch: (baseUrl, token, config, timeoutMs) => fetchCustom(baseUrl, token, config, customConfig, timeoutMs)
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/renderer/colors.ts
|
|
1159
|
+
var ANSI_COLORS = {
|
|
1160
|
+
black: "\x1B[30m",
|
|
1161
|
+
red: "\x1B[31m",
|
|
1162
|
+
green: "\x1B[32m",
|
|
1163
|
+
yellow: "\x1B[33m",
|
|
1164
|
+
blue: "\x1B[34m",
|
|
1165
|
+
magenta: "\x1B[35m",
|
|
1166
|
+
cyan: "\x1B[36m",
|
|
1167
|
+
white: "\x1B[37m",
|
|
1168
|
+
"bright-black": "\x1B[90m",
|
|
1169
|
+
"bright-red": "\x1B[91m",
|
|
1170
|
+
"bright-green": "\x1B[92m",
|
|
1171
|
+
"bright-yellow": "\x1B[93m",
|
|
1172
|
+
"bright-blue": "\x1B[94m",
|
|
1173
|
+
"bright-magenta": "\x1B[95m",
|
|
1174
|
+
"bright-cyan": "\x1B[96m",
|
|
1175
|
+
"bright-white": "\x1B[97m",
|
|
1176
|
+
gray: "\x1B[90m",
|
|
1177
|
+
grey: "\x1B[90m"
|
|
1178
|
+
};
|
|
1179
|
+
var ANSI_RESET = "\x1B[0m";
|
|
1180
|
+
var ANSI_DIM = "\x1B[2m";
|
|
1181
|
+
function ansiColor(text, color) {
|
|
1182
|
+
if (!color)
|
|
1183
|
+
return text;
|
|
1184
|
+
if (ANSI_COLORS[color.toLowerCase()]) {
|
|
1185
|
+
return `${ANSI_COLORS[color.toLowerCase()]}${text}${ANSI_RESET}`;
|
|
1186
|
+
}
|
|
1187
|
+
if (color.startsWith("#")) {
|
|
1188
|
+
const rgb = hexToRgb(color);
|
|
1189
|
+
if (rgb) {
|
|
1190
|
+
return `\x1B[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}${ANSI_RESET}`;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return text;
|
|
1194
|
+
}
|
|
1195
|
+
function hexToRgb(hex) {
|
|
1196
|
+
const cleanHex = hex.replace(/^#/, "");
|
|
1197
|
+
if (cleanHex.length === 3) {
|
|
1198
|
+
const [r, g, b] = cleanHex.split("").map((c) => parseInt(c + c, 16));
|
|
1199
|
+
if (isNaN(r) || isNaN(g) || isNaN(b))
|
|
1200
|
+
return null;
|
|
1201
|
+
return { r, g, b };
|
|
1202
|
+
}
|
|
1203
|
+
if (cleanHex.length === 6) {
|
|
1204
|
+
const r = parseInt(cleanHex.slice(0, 2), 16);
|
|
1205
|
+
const g = parseInt(cleanHex.slice(2, 4), 16);
|
|
1206
|
+
const b = parseInt(cleanHex.slice(4, 6), 16);
|
|
1207
|
+
if (isNaN(r) || isNaN(g) || isNaN(b))
|
|
1208
|
+
return null;
|
|
1209
|
+
return { r, g, b };
|
|
1210
|
+
}
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
function dimText(text) {
|
|
1214
|
+
return `${ANSI_DIM}${text}${ANSI_RESET}`;
|
|
1215
|
+
}
|
|
1216
|
+
function resolveColor(colorName, usagePercent, config) {
|
|
1217
|
+
const effectiveColor = colorName ?? "auto";
|
|
1218
|
+
if (effectiveColor.startsWith("#") || ANSI_COLORS[effectiveColor.toLowerCase()]) {
|
|
1219
|
+
return effectiveColor;
|
|
1220
|
+
}
|
|
1221
|
+
const alias = config.colors?.[effectiveColor];
|
|
1222
|
+
if (!alias) {
|
|
1223
|
+
const autoAlias = config.colors?.auto;
|
|
1224
|
+
if (typeof autoAlias === "string") {
|
|
1225
|
+
return autoAlias;
|
|
1226
|
+
}
|
|
1227
|
+
return resolveColorAlias(autoAlias, usagePercent);
|
|
1228
|
+
}
|
|
1229
|
+
if (typeof alias === "string") {
|
|
1230
|
+
return alias;
|
|
1231
|
+
}
|
|
1232
|
+
return resolveColorAlias(alias, usagePercent);
|
|
1233
|
+
}
|
|
1234
|
+
function resolveColorAlias(alias, usagePercent) {
|
|
1235
|
+
if (!alias)
|
|
1236
|
+
return null;
|
|
1237
|
+
if (usagePercent === null) {
|
|
1238
|
+
return alias.low;
|
|
1239
|
+
}
|
|
1240
|
+
if (usagePercent < alias.lowThreshold) {
|
|
1241
|
+
return alias.low;
|
|
1242
|
+
} else if (usagePercent < alias.highThreshold) {
|
|
1243
|
+
return alias.medium;
|
|
1244
|
+
} else {
|
|
1245
|
+
return alias.high;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/renderer/error.ts
|
|
1250
|
+
function renderError(errorState, mode, provider, message, cacheAge) {
|
|
1251
|
+
const isTransition = errorState === "switching-provider" || errorState === "new-credentials" || errorState === "new-endpoint" || errorState === "auth-error-waiting";
|
|
1252
|
+
if (isTransition) {
|
|
1253
|
+
return renderTransitionState(errorState);
|
|
1254
|
+
}
|
|
1255
|
+
if (mode === "without-cache") {
|
|
1256
|
+
return renderStandaloneError(errorState, provider, message);
|
|
1257
|
+
}
|
|
1258
|
+
return renderErrorIndicator(errorState, cacheAge);
|
|
1259
|
+
}
|
|
1260
|
+
function renderTransitionState(errorState) {
|
|
1261
|
+
const icon = "⟳";
|
|
1262
|
+
let message;
|
|
1263
|
+
switch (errorState) {
|
|
1264
|
+
case "switching-provider":
|
|
1265
|
+
message = "Switching provider...";
|
|
1266
|
+
break;
|
|
1267
|
+
case "new-credentials":
|
|
1268
|
+
message = "New credentials, refreshing...";
|
|
1269
|
+
break;
|
|
1270
|
+
case "new-endpoint":
|
|
1271
|
+
message = "New endpoint, refreshing...";
|
|
1272
|
+
break;
|
|
1273
|
+
case "auth-error-waiting":
|
|
1274
|
+
return `${ansiColor("⚠", "yellow")} Auth error ${dimText(`${icon} Waiting for new credentials...`)}`;
|
|
1275
|
+
default:
|
|
1276
|
+
message = "Transitioning...";
|
|
1277
|
+
}
|
|
1278
|
+
return dimText(`${icon} ${message}`);
|
|
1279
|
+
}
|
|
1280
|
+
function renderStandaloneError(errorState, provider, message) {
|
|
1281
|
+
const warningIcon = ansiColor("⚠", "yellow");
|
|
1282
|
+
switch (errorState) {
|
|
1283
|
+
case "auth-error":
|
|
1284
|
+
return `${warningIcon} Auth error`;
|
|
1285
|
+
case "rate-limited":
|
|
1286
|
+
return `${warningIcon} Rate limited`;
|
|
1287
|
+
case "provider-unknown":
|
|
1288
|
+
return `${warningIcon} Unknown provider`;
|
|
1289
|
+
case "missing-env":
|
|
1290
|
+
return `${warningIcon} Set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`;
|
|
1291
|
+
case "network-error":
|
|
1292
|
+
case "server-error":
|
|
1293
|
+
case "parse-error":
|
|
1294
|
+
if (provider && message) {
|
|
1295
|
+
return `${warningIcon} ${provider}: ${message}`;
|
|
1296
|
+
} else if (provider) {
|
|
1297
|
+
return `${warningIcon} ${provider}: ${getDefaultMessage(errorState)}`;
|
|
1298
|
+
} else {
|
|
1299
|
+
return `${warningIcon} ${getDefaultMessage(errorState)}`;
|
|
1300
|
+
}
|
|
1301
|
+
default:
|
|
1302
|
+
return `${warningIcon} Error`;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function renderErrorIndicator(errorState, cacheAge) {
|
|
1306
|
+
switch (errorState) {
|
|
1307
|
+
case "network-error":
|
|
1308
|
+
return renderStalenessIndicator("[offline]", cacheAge, false);
|
|
1309
|
+
case "server-error":
|
|
1310
|
+
return renderStalenessIndicator("[stale]", cacheAge, true);
|
|
1311
|
+
case "parse-error":
|
|
1312
|
+
return "[parse error]";
|
|
1313
|
+
case "rate-limited":
|
|
1314
|
+
return "[rate limited]";
|
|
1315
|
+
default:
|
|
1316
|
+
return "[error]";
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function renderStalenessIndicator(label, cacheAge, showAge = true) {
|
|
1320
|
+
if (cacheAge === undefined) {
|
|
1321
|
+
return dimText(label);
|
|
1322
|
+
}
|
|
1323
|
+
const stalenessLevel = getStalenessLevel(cacheAge);
|
|
1324
|
+
let text = label;
|
|
1325
|
+
if (showAge && cacheAge >= 5) {
|
|
1326
|
+
text = `[stale ${cacheAge}m]`;
|
|
1327
|
+
}
|
|
1328
|
+
switch (stalenessLevel) {
|
|
1329
|
+
case "fresh":
|
|
1330
|
+
return dimText(label);
|
|
1331
|
+
case "stale":
|
|
1332
|
+
return dimText(text);
|
|
1333
|
+
case "very-stale":
|
|
1334
|
+
return ansiColor(text, "yellow");
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
function getStalenessLevel(ageMinutes) {
|
|
1338
|
+
if (ageMinutes < 5) {
|
|
1339
|
+
return "fresh";
|
|
1340
|
+
} else if (ageMinutes <= 30) {
|
|
1341
|
+
return "stale";
|
|
1342
|
+
} else {
|
|
1343
|
+
return "very-stale";
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function getDefaultMessage(errorState) {
|
|
1347
|
+
switch (errorState) {
|
|
1348
|
+
case "network-error":
|
|
1349
|
+
return "connection refused";
|
|
1350
|
+
case "server-error":
|
|
1351
|
+
return "server error";
|
|
1352
|
+
case "parse-error":
|
|
1353
|
+
return "invalid response";
|
|
1354
|
+
case "auth-error":
|
|
1355
|
+
return "authentication failed";
|
|
1356
|
+
case "rate-limited":
|
|
1357
|
+
return "rate limited";
|
|
1358
|
+
default:
|
|
1359
|
+
return "error";
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/renderer/bar.ts
|
|
1364
|
+
function renderBar(percent, size, style, fillColor, emptyColor) {
|
|
1365
|
+
if (percent === -1) {
|
|
1366
|
+
return "";
|
|
1367
|
+
}
|
|
1368
|
+
const effectivePercent = percent ?? 0;
|
|
1369
|
+
const clampedPercent = Math.max(0, Math.min(100, effectivePercent));
|
|
1370
|
+
const barWidth = BAR_SIZE_MAP[size];
|
|
1371
|
+
const barChars = resolveBarStyle(style);
|
|
1372
|
+
if (typeof style === "string" && style === "braille") {
|
|
1373
|
+
return renderBrailleBar(clampedPercent, barWidth, fillColor, emptyColor);
|
|
1374
|
+
}
|
|
1375
|
+
const filledCount = Math.round(clampedPercent / 100 * barWidth);
|
|
1376
|
+
const emptyCount = barWidth - filledCount;
|
|
1377
|
+
const filledPart = barChars.fill.repeat(filledCount);
|
|
1378
|
+
const coloredFilled = fillColor ? ansiColor(filledPart, fillColor) : filledPart;
|
|
1379
|
+
const emptyPart = barChars.empty.repeat(emptyCount);
|
|
1380
|
+
const coloredEmpty = emptyColor ? ansiColor(emptyPart, emptyColor) : dimText(emptyPart);
|
|
1381
|
+
return coloredFilled + coloredEmpty;
|
|
1382
|
+
}
|
|
1383
|
+
function resolveBarStyle(style) {
|
|
1384
|
+
if (typeof style === "string") {
|
|
1385
|
+
return BUILT_IN_BAR_STYLES[style] ?? BUILT_IN_BAR_STYLES.classic;
|
|
1386
|
+
}
|
|
1387
|
+
return style;
|
|
1388
|
+
}
|
|
1389
|
+
function renderBrailleBar(percent, barWidth, fillColor, emptyColor) {
|
|
1390
|
+
const brailleChars = ["⣀", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"];
|
|
1391
|
+
const emptyChar = "⣀";
|
|
1392
|
+
const fullChar = "⣿";
|
|
1393
|
+
const exactFill = percent / 100 * barWidth;
|
|
1394
|
+
const fullCells = Math.floor(exactFill);
|
|
1395
|
+
const remainder = exactFill - fullCells;
|
|
1396
|
+
let bar = "";
|
|
1397
|
+
for (let i = 0;i < fullCells; i++) {
|
|
1398
|
+
bar += fullChar;
|
|
1399
|
+
}
|
|
1400
|
+
if (fullCells < barWidth && remainder > 0) {
|
|
1401
|
+
const gradientIndex = Math.floor(remainder * (brailleChars.length - 1));
|
|
1402
|
+
bar += brailleChars[gradientIndex] ?? emptyChar;
|
|
1403
|
+
}
|
|
1404
|
+
const emptyCells = barWidth - fullCells - (remainder > 0 ? 1 : 0);
|
|
1405
|
+
for (let i = 0;i < emptyCells; i++) {
|
|
1406
|
+
bar += emptyChar;
|
|
1407
|
+
}
|
|
1408
|
+
const filledPart = bar.slice(0, fullCells + (remainder > 0 ? 1 : 0));
|
|
1409
|
+
const emptyPart = bar.slice(fullCells + (remainder > 0 ? 1 : 0));
|
|
1410
|
+
const coloredFilled = fillColor ? ansiColor(filledPart, fillColor) : filledPart;
|
|
1411
|
+
const coloredEmpty = emptyColor ? ansiColor(emptyPart, emptyColor) : dimText(emptyPart);
|
|
1412
|
+
return coloredFilled + coloredEmpty;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/renderer/countdown.ts
|
|
1416
|
+
function renderCountdown(resetsAt, config, clockFormat) {
|
|
1417
|
+
if (!resetsAt) {
|
|
1418
|
+
return "";
|
|
1419
|
+
}
|
|
1420
|
+
const format = config.format ?? "auto";
|
|
1421
|
+
const divider = config.divider ?? " · ";
|
|
1422
|
+
const prefix = config.prefix ?? "";
|
|
1423
|
+
const resetDate = new Date(resetsAt);
|
|
1424
|
+
if (isNaN(resetDate.getTime())) {
|
|
1425
|
+
return "";
|
|
1426
|
+
}
|
|
1427
|
+
const now = new Date;
|
|
1428
|
+
const remainingMs = resetDate.getTime() - now.getTime();
|
|
1429
|
+
if (remainingMs < 0) {
|
|
1430
|
+
return `${divider}${prefix}now`;
|
|
1431
|
+
}
|
|
1432
|
+
let timeStr;
|
|
1433
|
+
if (format === "auto") {
|
|
1434
|
+
if (remainingMs < 60000) {
|
|
1435
|
+
timeStr = "now";
|
|
1436
|
+
} else if (remainingMs <= 86400000) {
|
|
1437
|
+
timeStr = formatDuration(remainingMs);
|
|
1438
|
+
} else {
|
|
1439
|
+
timeStr = formatWallClock(resetDate, clockFormat);
|
|
1440
|
+
}
|
|
1441
|
+
} else if (format === "duration") {
|
|
1442
|
+
if (remainingMs < 60000) {
|
|
1443
|
+
timeStr = "now";
|
|
1444
|
+
} else {
|
|
1445
|
+
timeStr = formatDuration(remainingMs);
|
|
1446
|
+
}
|
|
1447
|
+
} else {
|
|
1448
|
+
timeStr = formatWallClock(resetDate, clockFormat);
|
|
1449
|
+
}
|
|
1450
|
+
return `${divider}${prefix}${timeStr}`;
|
|
1451
|
+
}
|
|
1452
|
+
function formatDuration(ms) {
|
|
1453
|
+
const seconds = Math.floor(ms / 1000);
|
|
1454
|
+
const minutes = Math.floor(seconds / 60);
|
|
1455
|
+
const hours = Math.floor(minutes / 60);
|
|
1456
|
+
const days = Math.floor(hours / 24);
|
|
1457
|
+
if (days >= 1) {
|
|
1458
|
+
const remainingHours = hours % 24;
|
|
1459
|
+
return `${days}d ${remainingHours}h`;
|
|
1460
|
+
} else if (hours >= 1) {
|
|
1461
|
+
const remainingMinutes = minutes % 60;
|
|
1462
|
+
return `${hours}h${remainingMinutes}m`;
|
|
1463
|
+
} else {
|
|
1464
|
+
return `${minutes}m`;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function formatWallClock(date, clockFormat) {
|
|
1468
|
+
const now = new Date;
|
|
1469
|
+
const dayOfWeek = date.toLocaleDateString("en-US", { weekday: "short" });
|
|
1470
|
+
const month = date.toLocaleDateString("en-US", { month: "short" });
|
|
1471
|
+
const dayOfMonth = date.getDate();
|
|
1472
|
+
const isSameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate();
|
|
1473
|
+
const isSameMonth = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth();
|
|
1474
|
+
let timeStr;
|
|
1475
|
+
if (clockFormat === "12h") {
|
|
1476
|
+
const hours = date.getHours();
|
|
1477
|
+
const minutes = date.getMinutes();
|
|
1478
|
+
const ampm = hours >= 12 ? "pm" : "am";
|
|
1479
|
+
const hour12 = hours % 12 || 12;
|
|
1480
|
+
if (minutes === 0) {
|
|
1481
|
+
timeStr = `${hour12}${ampm}`;
|
|
1482
|
+
} else {
|
|
1483
|
+
timeStr = `${hour12}:${minutes.toString().padStart(2, "0")}${ampm}`;
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
const hours = date.getHours();
|
|
1487
|
+
const minutes = date.getMinutes();
|
|
1488
|
+
timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
|
1489
|
+
}
|
|
1490
|
+
if (isSameDay) {
|
|
1491
|
+
return `${dayOfWeek} ${timeStr}`;
|
|
1492
|
+
} else if (isSameMonth) {
|
|
1493
|
+
return `${dayOfWeek} ${dayOfMonth} ${timeStr}`;
|
|
1494
|
+
} else {
|
|
1495
|
+
return `${month} ${dayOfMonth} ${timeStr}`;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/renderer/icons.ts
|
|
1500
|
+
var PROGRESS_ICONS = [
|
|
1501
|
+
"\uDB80\uDD30",
|
|
1502
|
+
"\uDB82\uDE9E",
|
|
1503
|
+
"\uDB82\uDE9F",
|
|
1504
|
+
"\uDB82\uDEA0",
|
|
1505
|
+
"\uDB82\uDEA1",
|
|
1506
|
+
"\uDB82\uDEA2",
|
|
1507
|
+
"\uDB82\uDEA3",
|
|
1508
|
+
"\uDB82\uDEA4",
|
|
1509
|
+
"\uDB82\uDEA5"
|
|
1510
|
+
];
|
|
1511
|
+
function getProgressIcon(percent) {
|
|
1512
|
+
if (percent === null) {
|
|
1513
|
+
return PROGRESS_ICONS[0] ?? "";
|
|
1514
|
+
}
|
|
1515
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
1516
|
+
const index = Math.min(8, Math.ceil(clampedPercent / 12.5));
|
|
1517
|
+
return PROGRESS_ICONS[index] ?? PROGRESS_ICONS[0] ?? "";
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/renderer/component.ts
|
|
1521
|
+
function renderComponent(componentId, data, componentConfig, globalConfig) {
|
|
1522
|
+
const effectiveLayout = componentConfig.layout ?? globalConfig.display.layout;
|
|
1523
|
+
const effectiveDisplayMode = componentConfig.displayMode ?? globalConfig.display.displayMode;
|
|
1524
|
+
const effectiveBarSize = componentConfig.barSize ?? globalConfig.display.barSize;
|
|
1525
|
+
const effectiveBarStyle = componentConfig.barStyle ?? globalConfig.display.barStyle;
|
|
1526
|
+
const clockFormat = globalConfig.display.clockFormat;
|
|
1527
|
+
switch (componentId) {
|
|
1528
|
+
case "daily":
|
|
1529
|
+
return renderQuotaComponent("daily", data.daily, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat);
|
|
1530
|
+
case "weekly":
|
|
1531
|
+
return renderQuotaComponent("weekly", data.weekly, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat);
|
|
1532
|
+
case "monthly":
|
|
1533
|
+
return renderQuotaComponent("monthly", data.monthly, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat);
|
|
1534
|
+
case "balance":
|
|
1535
|
+
return renderBalanceComponent(data.balance, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig);
|
|
1536
|
+
case "tokens":
|
|
1537
|
+
return renderTokensComponent(data.tokenStats, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig);
|
|
1538
|
+
case "rateLimit":
|
|
1539
|
+
return renderRateLimitComponent(data.rateLimit, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig);
|
|
1540
|
+
case "plan":
|
|
1541
|
+
return renderPlanComponent(data.planName, effectiveLayout, componentConfig, globalConfig);
|
|
1542
|
+
default:
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function renderQuotaComponent(componentId, quota, layout, displayMode, barSize, barStyle, componentConfig, globalConfig, clockFormat) {
|
|
1547
|
+
if (!quota)
|
|
1548
|
+
return null;
|
|
1549
|
+
const usagePercent = calculateUsagePercent(quota.used, quota.limit);
|
|
1550
|
+
const label = renderLabel(componentId, layout, componentConfig, globalConfig, displayMode);
|
|
1551
|
+
const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
|
|
1552
|
+
const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
|
|
1553
|
+
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
1554
|
+
const countdownColor = resolvePartColor("countdown", usagePercent, componentConfig, globalConfig);
|
|
1555
|
+
const display = renderDisplayMode(displayMode, usagePercent, barSize, barStyle, barColor, null);
|
|
1556
|
+
const value = ansiColor(`${Math.round(usagePercent)}%`, valueColor);
|
|
1557
|
+
const countdown = renderCountdownSubComponent(quota.resetsAt, componentConfig.countdown, countdownColor, clockFormat);
|
|
1558
|
+
return assembleComponent(layout, label, labelColor, display, value, countdown);
|
|
1559
|
+
}
|
|
1560
|
+
function renderBalanceComponent(balance, layout, displayMode, barSize, barStyle, componentConfig, globalConfig) {
|
|
1561
|
+
if (!balance)
|
|
1562
|
+
return null;
|
|
1563
|
+
const isUnlimited = balance.remaining === -1;
|
|
1564
|
+
let usagePercent = null;
|
|
1565
|
+
if (!isUnlimited && balance.initial !== null && balance.initial > 0) {
|
|
1566
|
+
usagePercent = (balance.initial - balance.remaining) / balance.initial * 100;
|
|
1567
|
+
}
|
|
1568
|
+
const effectivePercent = isUnlimited ? 0 : usagePercent;
|
|
1569
|
+
const label = renderLabel("balance", layout, componentConfig, globalConfig, displayMode);
|
|
1570
|
+
const barColor = resolvePartColor("bar", effectivePercent, componentConfig, globalConfig);
|
|
1571
|
+
const valueColor = resolvePartColor("value", effectivePercent, componentConfig, globalConfig);
|
|
1572
|
+
const labelColor = resolvePartColor("label", effectivePercent, componentConfig, globalConfig);
|
|
1573
|
+
const display = isUnlimited ? "" : renderDisplayMode(displayMode, effectivePercent ?? 0, barSize, barStyle, barColor, null);
|
|
1574
|
+
const valueText = isUnlimited ? "∞" : `$${balance.remaining.toFixed(2)}`;
|
|
1575
|
+
const value = ansiColor(valueText, valueColor);
|
|
1576
|
+
const countdown = "";
|
|
1577
|
+
return assembleComponent(layout, label, labelColor, display, value, countdown);
|
|
1578
|
+
}
|
|
1579
|
+
function renderTokensComponent(tokenStats, layout, displayMode, componentConfig, globalConfig) {
|
|
1580
|
+
if (!tokenStats)
|
|
1581
|
+
return null;
|
|
1582
|
+
const stats = tokenStats.total ?? tokenStats.today;
|
|
1583
|
+
if (!stats)
|
|
1584
|
+
return null;
|
|
1585
|
+
const label = renderLabel("tokens", layout, componentConfig, globalConfig, displayMode);
|
|
1586
|
+
const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
|
|
1587
|
+
const valueColor = resolvePartColor("value", null, componentConfig, globalConfig);
|
|
1588
|
+
const tokenCount = stats.totalTokens ?? stats.inputTokens + stats.outputTokens;
|
|
1589
|
+
const valueText = formatLargeNumber(tokenCount);
|
|
1590
|
+
const value = ansiColor(valueText, valueColor);
|
|
1591
|
+
const display = "";
|
|
1592
|
+
const countdown = "";
|
|
1593
|
+
return assembleComponent(layout, label, labelColor, display, value, countdown);
|
|
1594
|
+
}
|
|
1595
|
+
function renderRateLimitComponent(rateLimit, layout, displayMode, barSize, barStyle, componentConfig, globalConfig) {
|
|
1596
|
+
if (!rateLimit)
|
|
1597
|
+
return null;
|
|
1598
|
+
let usagePercent = null;
|
|
1599
|
+
if (rateLimit.requestsLimit !== null && rateLimit.requestsLimit > 0) {
|
|
1600
|
+
usagePercent = rateLimit.requestsUsed / rateLimit.requestsLimit * 100;
|
|
1601
|
+
}
|
|
1602
|
+
const label = renderLabel("rateLimit", layout, componentConfig, globalConfig, displayMode);
|
|
1603
|
+
const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
|
|
1604
|
+
const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
|
|
1605
|
+
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
1606
|
+
const display = usagePercent !== null ? renderDisplayMode(displayMode, usagePercent, barSize, barStyle, barColor, null) : "";
|
|
1607
|
+
const valueText = rateLimit.requestsLimit !== null ? `${rateLimit.requestsUsed}/${rateLimit.requestsLimit}` : `${rateLimit.requestsUsed}`;
|
|
1608
|
+
const value = ansiColor(valueText, valueColor);
|
|
1609
|
+
const countdown = "";
|
|
1610
|
+
return assembleComponent(layout, label, labelColor, display, value, countdown);
|
|
1611
|
+
}
|
|
1612
|
+
function renderPlanComponent(planName, layout, componentConfig, globalConfig) {
|
|
1613
|
+
if (layout === "minimal")
|
|
1614
|
+
return null;
|
|
1615
|
+
const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
|
|
1616
|
+
const value = ansiColor(planName, labelColor);
|
|
1617
|
+
if (typeof componentConfig.label === "object" && componentConfig.label.text) {
|
|
1618
|
+
const labelText = ansiColor(componentConfig.label.text, labelColor);
|
|
1619
|
+
return `${labelText} ${value}`;
|
|
1620
|
+
}
|
|
1621
|
+
return value;
|
|
1622
|
+
}
|
|
1623
|
+
function renderLabel(componentId, layout, componentConfig, globalConfig, displayMode) {
|
|
1624
|
+
if (layout === "minimal")
|
|
1625
|
+
return "";
|
|
1626
|
+
if (componentConfig.label === false)
|
|
1627
|
+
return "";
|
|
1628
|
+
if (displayMode === "icon-pct" && typeof componentConfig.label === "object" && componentConfig.label.icon) {
|
|
1629
|
+
return componentConfig.label.icon;
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof componentConfig.label === "string") {
|
|
1632
|
+
return layout === "compact" ? componentConfig.label.charAt(0) : componentConfig.label;
|
|
1633
|
+
}
|
|
1634
|
+
if (typeof componentConfig.label === "object" && componentConfig.label.text) {
|
|
1635
|
+
return layout === "compact" ? componentConfig.label.text.charAt(0) : componentConfig.label.text;
|
|
1636
|
+
}
|
|
1637
|
+
if (layout === "compact") {
|
|
1638
|
+
return COMPONENT_SHORT_LABELS[componentId] ?? "";
|
|
1639
|
+
}
|
|
1640
|
+
return COMPONENT_FULL_LABELS[componentId] ?? "";
|
|
1641
|
+
}
|
|
1642
|
+
function renderDisplayMode(displayMode, usagePercent, barSize, barStyle, barColor, emptyColor) {
|
|
1643
|
+
switch (displayMode) {
|
|
1644
|
+
case "bar":
|
|
1645
|
+
return renderBar(usagePercent, barSize, barStyle, barColor, emptyColor);
|
|
1646
|
+
case "percentage":
|
|
1647
|
+
return "";
|
|
1648
|
+
case "icon-pct":
|
|
1649
|
+
return getProgressIcon(usagePercent);
|
|
1650
|
+
default:
|
|
1651
|
+
return "";
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function renderCountdownSubComponent(resetsAt, countdownConfig, countdownColor, clockFormat) {
|
|
1655
|
+
if (countdownConfig === false || !resetsAt)
|
|
1656
|
+
return "";
|
|
1657
|
+
const config = typeof countdownConfig === "object" ? countdownConfig : {};
|
|
1658
|
+
const countdown = renderCountdown(resetsAt, config, clockFormat);
|
|
1659
|
+
return countdownColor ? ansiColor(countdown, countdownColor) : countdown;
|
|
1660
|
+
}
|
|
1661
|
+
function assembleComponent(layout, label, labelColor, display, value, countdown) {
|
|
1662
|
+
const coloredLabel = label && labelColor ? ansiColor(label, labelColor) : label;
|
|
1663
|
+
const parts = [];
|
|
1664
|
+
if (layout === "minimal") {
|
|
1665
|
+
if (display)
|
|
1666
|
+
parts.push(display);
|
|
1667
|
+
parts.push(value);
|
|
1668
|
+
if (countdown)
|
|
1669
|
+
parts.push(countdown);
|
|
1670
|
+
} else if (layout === "percent-first") {
|
|
1671
|
+
if (coloredLabel)
|
|
1672
|
+
parts.push(coloredLabel);
|
|
1673
|
+
parts.push(value);
|
|
1674
|
+
if (display)
|
|
1675
|
+
parts.push(display);
|
|
1676
|
+
if (countdown)
|
|
1677
|
+
parts.push(countdown);
|
|
1678
|
+
} else {
|
|
1679
|
+
if (coloredLabel)
|
|
1680
|
+
parts.push(coloredLabel);
|
|
1681
|
+
if (display)
|
|
1682
|
+
parts.push(display);
|
|
1683
|
+
parts.push(value);
|
|
1684
|
+
if (countdown)
|
|
1685
|
+
parts.push(countdown);
|
|
1686
|
+
}
|
|
1687
|
+
return parts.filter((p) => p).join(" ").replace(/ (\x1b\[[0-9;]*m)? ([·•])/g, "$1 $2");
|
|
1688
|
+
}
|
|
1689
|
+
function calculateUsagePercent(used, limit) {
|
|
1690
|
+
if (limit === null || limit === 0)
|
|
1691
|
+
return 0;
|
|
1692
|
+
return used / limit * 100;
|
|
1693
|
+
}
|
|
1694
|
+
function resolvePartColor(part, usagePercent, componentConfig, globalConfig) {
|
|
1695
|
+
if (componentConfig.colors && componentConfig.colors[part]) {
|
|
1696
|
+
const partColor = componentConfig.colors[part];
|
|
1697
|
+
return resolveColor(partColor ?? null, usagePercent, globalConfig);
|
|
1698
|
+
}
|
|
1699
|
+
const color = componentConfig.color ?? "auto";
|
|
1700
|
+
return resolveColor(color, usagePercent, globalConfig);
|
|
1701
|
+
}
|
|
1702
|
+
function formatLargeNumber(n) {
|
|
1703
|
+
if (n >= 1e9) {
|
|
1704
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
1705
|
+
} else if (n >= 1e6) {
|
|
1706
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1707
|
+
} else if (n >= 1000) {
|
|
1708
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
1709
|
+
}
|
|
1710
|
+
return n.toString();
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/renderer/truncate.ts
|
|
1714
|
+
function getTerminalWidth() {
|
|
1715
|
+
if (process.stdout.columns && process.stdout.columns > 0) {
|
|
1716
|
+
return process.stdout.columns;
|
|
1717
|
+
}
|
|
1718
|
+
return 80;
|
|
1719
|
+
}
|
|
1720
|
+
function computeMaxWidth(termWidth, maxWidthPct) {
|
|
1721
|
+
const pct = Math.max(20, Math.min(100, maxWidthPct));
|
|
1722
|
+
return Math.floor(termWidth * pct / 100);
|
|
1723
|
+
}
|
|
1724
|
+
function visibleLength(text) {
|
|
1725
|
+
const stripped = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1726
|
+
return stripped.length;
|
|
1727
|
+
}
|
|
1728
|
+
function ansiAwareTruncate(text, maxWidth) {
|
|
1729
|
+
const visible = visibleLength(text);
|
|
1730
|
+
if (visible <= maxWidth) {
|
|
1731
|
+
return text;
|
|
1732
|
+
}
|
|
1733
|
+
const targetWidth = maxWidth - 1;
|
|
1734
|
+
let output = "";
|
|
1735
|
+
let visibleCount = 0;
|
|
1736
|
+
let i = 0;
|
|
1737
|
+
while (i < text.length && visibleCount < targetWidth) {
|
|
1738
|
+
if (text[i] === "\x1B" && text[i + 1] === "[") {
|
|
1739
|
+
const escapeStart = i;
|
|
1740
|
+
i += 2;
|
|
1741
|
+
while (i < text.length && text[i] !== undefined && /[0-9;]/.test(text[i])) {
|
|
1742
|
+
i++;
|
|
1743
|
+
}
|
|
1744
|
+
if (i < text.length) {
|
|
1745
|
+
i++;
|
|
1746
|
+
}
|
|
1747
|
+
output += text.slice(escapeStart, i);
|
|
1748
|
+
} else {
|
|
1749
|
+
const char = text[i];
|
|
1750
|
+
if (char !== undefined) {
|
|
1751
|
+
output += char;
|
|
1752
|
+
}
|
|
1753
|
+
visibleCount++;
|
|
1754
|
+
i++;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return output + "…";
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/renderer/index.ts
|
|
1761
|
+
var DEFAULT_COMPONENT_ORDER2 = [
|
|
1762
|
+
"daily",
|
|
1763
|
+
"weekly",
|
|
1764
|
+
"monthly",
|
|
1765
|
+
"balance",
|
|
1766
|
+
"tokens",
|
|
1767
|
+
"rateLimit",
|
|
1768
|
+
"plan"
|
|
1769
|
+
];
|
|
1770
|
+
function renderStatusline(data, config, errorState, cacheAge) {
|
|
1771
|
+
const componentOrder = getComponentOrder(config);
|
|
1772
|
+
const renderedComponents = [];
|
|
1773
|
+
for (const componentId of componentOrder) {
|
|
1774
|
+
const componentConfig = config.components[componentId];
|
|
1775
|
+
if (componentConfig === false) {
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
const rendered = renderComponent(componentId, data, componentConfig === true || componentConfig === undefined ? {} : componentConfig, config);
|
|
1779
|
+
if (rendered !== null) {
|
|
1780
|
+
renderedComponents.push(rendered);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
const separator = config.display.separator ?? " | ";
|
|
1784
|
+
let statusline = renderedComponents.join(separator);
|
|
1785
|
+
if (errorState) {
|
|
1786
|
+
const isTransition = errorState === "switching-provider" || errorState === "new-credentials" || errorState === "new-endpoint" || errorState === "auth-error-waiting";
|
|
1787
|
+
if (isTransition) {
|
|
1788
|
+
statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
|
|
1789
|
+
} else {
|
|
1790
|
+
const hasCache = renderedComponents.length > 0;
|
|
1791
|
+
const errorMode = hasCache ? "with-cache" : "without-cache";
|
|
1792
|
+
const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
|
|
1793
|
+
if (hasCache) {
|
|
1794
|
+
statusline = `${statusline} ${errorIndicator}`;
|
|
1795
|
+
} else {
|
|
1796
|
+
statusline = errorIndicator;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const termWidth = getTerminalWidth();
|
|
1801
|
+
const maxWidth = computeMaxWidth(termWidth, config.display.maxWidth ?? 80);
|
|
1802
|
+
statusline = ansiAwareTruncate(statusline, maxWidth);
|
|
1803
|
+
return statusline;
|
|
1804
|
+
}
|
|
1805
|
+
function getComponentOrder(config) {
|
|
1806
|
+
const explicitOrder = [];
|
|
1807
|
+
const explicitSet = new Set;
|
|
1808
|
+
for (const key of Object.keys(config.components)) {
|
|
1809
|
+
if (isComponentId(key)) {
|
|
1810
|
+
explicitOrder.push(key);
|
|
1811
|
+
explicitSet.add(key);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const order = [...explicitOrder];
|
|
1815
|
+
for (const componentId of DEFAULT_COMPONENT_ORDER2) {
|
|
1816
|
+
if (!explicitSet.has(componentId)) {
|
|
1817
|
+
order.push(componentId);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return order;
|
|
1821
|
+
}
|
|
1822
|
+
function isComponentId(key) {
|
|
1823
|
+
return DEFAULT_COMPONENT_ORDER2.includes(key);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/core/execute-cycle.ts
|
|
1827
|
+
async function executeCycle(ctx) {
|
|
1828
|
+
const { env, config, configHash, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
1829
|
+
if (cachedEntry) {
|
|
1830
|
+
if (isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
1831
|
+
if (cachedEntry.renderedLine && isCacheRenderedLineUsable(cachedEntry, configHash)) {
|
|
1832
|
+
logger.debug("Path A: Fast path (cached renderedLine)", {
|
|
1833
|
+
cacheAge: `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s`
|
|
1834
|
+
});
|
|
1835
|
+
return {
|
|
1836
|
+
output: cachedEntry.renderedLine,
|
|
1837
|
+
exitCode: 0,
|
|
1838
|
+
cacheUpdate: null
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
1844
|
+
logger.debug("Path B: Re-render (config changed, cache data valid)");
|
|
1845
|
+
const statusline = renderStatusline(cachedEntry.data, config);
|
|
1846
|
+
const updatedEntry = {
|
|
1847
|
+
...cachedEntry,
|
|
1848
|
+
renderedLine: statusline,
|
|
1849
|
+
configHash
|
|
1850
|
+
};
|
|
1851
|
+
return {
|
|
1852
|
+
output: statusline,
|
|
1853
|
+
exitCode: 0,
|
|
1854
|
+
cacheUpdate: updatedEntry
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
const deadline = startTime + timeoutBudgetMs - 50;
|
|
1858
|
+
const remainingBudget = deadline - Date.now();
|
|
1859
|
+
if (remainingBudget <= 50) {
|
|
1860
|
+
logger.debug("Path D: Timeout fallback (insufficient budget)", { remainingBudget });
|
|
1861
|
+
if (cachedEntry && cachedEntry.renderedLine) {
|
|
1862
|
+
return {
|
|
1863
|
+
output: cachedEntry.renderedLine,
|
|
1864
|
+
exitCode: 0,
|
|
1865
|
+
cacheUpdate: null
|
|
1866
|
+
};
|
|
1867
|
+
} else {
|
|
1868
|
+
return {
|
|
1869
|
+
output: "[loading...]",
|
|
1870
|
+
exitCode: 0,
|
|
1871
|
+
cacheUpdate: null
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
try {
|
|
1876
|
+
const baseUrl = env.baseUrl;
|
|
1877
|
+
const authToken = env.authToken;
|
|
1878
|
+
if (!baseUrl || !authToken) {
|
|
1879
|
+
return {
|
|
1880
|
+
output: renderError("missing-env", "without-cache"),
|
|
1881
|
+
exitCode: 1,
|
|
1882
|
+
cacheUpdate: null
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
logger.debug("Path C: Fetching from provider", { providerId, fetchTimeoutMs });
|
|
1886
|
+
const fetchStart = Date.now();
|
|
1887
|
+
const data = await provider.fetch(baseUrl, authToken, config, fetchTimeoutMs);
|
|
1888
|
+
const fetchTime = Date.now() - fetchStart;
|
|
1889
|
+
logger.debug("Fetch completed", { fetchTime: `${fetchTime}ms` });
|
|
1890
|
+
const statusline = renderStatusline(data, config);
|
|
1891
|
+
const ttlSeconds = getEffectivePollInterval(config, env.pollIntervalOverride);
|
|
1892
|
+
const newEntry = {
|
|
1893
|
+
version: CACHE_VERSION,
|
|
1894
|
+
baseUrl,
|
|
1895
|
+
tokenHash: env.tokenHash ?? "",
|
|
1896
|
+
provider: providerId,
|
|
1897
|
+
fetchedAt: data.fetchedAt,
|
|
1898
|
+
ttlSeconds,
|
|
1899
|
+
data,
|
|
1900
|
+
renderedLine: statusline,
|
|
1901
|
+
configHash
|
|
1902
|
+
};
|
|
1903
|
+
return {
|
|
1904
|
+
output: statusline,
|
|
1905
|
+
exitCode: 0,
|
|
1906
|
+
cacheUpdate: newEntry
|
|
1907
|
+
};
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
logger.error("Path D: Fetch error", { error: String(error), hasCachedEntry: !!cachedEntry });
|
|
1910
|
+
if (cachedEntry) {
|
|
1911
|
+
const ageMinutes = Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 60000);
|
|
1912
|
+
const statusline = renderStatusline(cachedEntry.data, config, "network-error", ageMinutes);
|
|
1913
|
+
logger.debug("Using stale cache with error indicator", { ageMinutes });
|
|
1914
|
+
return {
|
|
1915
|
+
output: statusline,
|
|
1916
|
+
exitCode: 0,
|
|
1917
|
+
cacheUpdate: null
|
|
1918
|
+
};
|
|
1919
|
+
} else {
|
|
1920
|
+
logger.warn("No cache available for error fallback");
|
|
1921
|
+
const errorOutput = renderError("network-error", "without-cache", providerId);
|
|
1922
|
+
return {
|
|
1923
|
+
output: errorOutput,
|
|
1924
|
+
exitCode: 1,
|
|
1925
|
+
cacheUpdate: null
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
// src/services/settings.ts
|
|
1931
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, renameSync as renameSync3, unlinkSync as unlinkSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1932
|
+
import { dirname as dirname2 } from "path";
|
|
1933
|
+
import { execSync as execSync2 } from "child_process";
|
|
1934
|
+
function loadClaudeSettings() {
|
|
1935
|
+
const path = getSettingsJsonPath();
|
|
1936
|
+
if (!existsSync5(path)) {
|
|
1937
|
+
return {};
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
const content = readFileSync4(path, "utf-8");
|
|
1941
|
+
return JSON.parse(content);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
console.warn(`Failed to read settings from ${path}: ${error}`);
|
|
1944
|
+
return {};
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function saveClaudeSettings(settings) {
|
|
1948
|
+
const path = getSettingsJsonPath();
|
|
1949
|
+
const tmpPath = `${path}.tmp`;
|
|
1950
|
+
try {
|
|
1951
|
+
const dir = dirname2(path);
|
|
1952
|
+
if (!existsSync5(dir)) {
|
|
1953
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
1954
|
+
}
|
|
1955
|
+
const content = JSON.stringify(settings, null, 2) + `
|
|
1956
|
+
`;
|
|
1957
|
+
writeFileSync3(tmpPath, content, { encoding: "utf-8", mode: 384 });
|
|
1958
|
+
renameSync3(tmpPath, path);
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
console.error(`Failed to write settings to ${path}: ${error}`);
|
|
1961
|
+
try {
|
|
1962
|
+
if (existsSync5(tmpPath)) {
|
|
1963
|
+
unlinkSync3(tmpPath);
|
|
1964
|
+
}
|
|
1965
|
+
} catch {}
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
function getExistingStatusLine() {
|
|
1970
|
+
const settings = loadClaudeSettings();
|
|
1971
|
+
return settings.statusLine?.command ?? null;
|
|
1972
|
+
}
|
|
1973
|
+
function isBunxAvailable() {
|
|
1974
|
+
try {
|
|
1975
|
+
execSync2("which bunx", { stdio: "ignore" });
|
|
1976
|
+
return true;
|
|
1977
|
+
} catch {
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function installStatusLine(runner) {
|
|
1982
|
+
const settings = loadClaudeSettings();
|
|
1983
|
+
settings.statusLine = {
|
|
1984
|
+
type: "command",
|
|
1985
|
+
command: `${runner} -y cc-api-statusline@latest`,
|
|
1986
|
+
padding: 0
|
|
1987
|
+
};
|
|
1988
|
+
saveClaudeSettings(settings);
|
|
1989
|
+
}
|
|
1990
|
+
function uninstallStatusLine() {
|
|
1991
|
+
const settings = loadClaudeSettings();
|
|
1992
|
+
if ("statusLine" in settings) {
|
|
1993
|
+
delete settings.statusLine;
|
|
1994
|
+
saveClaudeSettings(settings);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
// package.json
|
|
1998
|
+
var package_default = {
|
|
1999
|
+
name: "cc-api-statusline",
|
|
2000
|
+
version: "0.1.0",
|
|
2001
|
+
description: "Claude Code statusline tool that polls API usage from third-party proxy backends",
|
|
2002
|
+
type: "module",
|
|
2003
|
+
bin: {
|
|
2004
|
+
"cc-api-statusline": "dist/cc-api-statusline.js"
|
|
2005
|
+
},
|
|
2006
|
+
scripts: {
|
|
2007
|
+
start: "bun run src/main.ts --once",
|
|
2008
|
+
dev: "bun run src/main.ts",
|
|
2009
|
+
example: "cat docs/fixtures/ccstatusline-context.sample.json | bun run src/main.ts",
|
|
2010
|
+
test: "bun run build && vitest run",
|
|
2011
|
+
"test:watch": "vitest",
|
|
2012
|
+
lint: "eslint src",
|
|
2013
|
+
build: "bun build src/main.ts --target=node --outfile=dist/cc-api-statusline.js",
|
|
2014
|
+
check: "bun run test && bun run lint",
|
|
2015
|
+
prepublishOnly: "bun run check"
|
|
2016
|
+
},
|
|
2017
|
+
files: [
|
|
2018
|
+
"dist/"
|
|
2019
|
+
],
|
|
2020
|
+
keywords: [
|
|
2021
|
+
"claude",
|
|
2022
|
+
"statusline",
|
|
2023
|
+
"api",
|
|
2024
|
+
"usage",
|
|
2025
|
+
"monitoring",
|
|
2026
|
+
"tui",
|
|
2027
|
+
"cli"
|
|
2028
|
+
],
|
|
2029
|
+
author: "Liafonx",
|
|
2030
|
+
license: "MIT",
|
|
2031
|
+
repository: {
|
|
2032
|
+
type: "git",
|
|
2033
|
+
url: "https://github.com/liafonx/cc-api-statusline.git"
|
|
2034
|
+
},
|
|
2035
|
+
homepage: "https://github.com/liafonx/cc-api-statusline#readme",
|
|
2036
|
+
bugs: {
|
|
2037
|
+
url: "https://github.com/liafonx/cc-api-statusline/issues"
|
|
2038
|
+
},
|
|
2039
|
+
dependencies: {},
|
|
2040
|
+
devDependencies: {
|
|
2041
|
+
"@eslint/js": "^9.17.0",
|
|
2042
|
+
"@types/bun": "^1.1.14",
|
|
2043
|
+
eslint: "^9.17.0",
|
|
2044
|
+
typescript: "^5.7.2",
|
|
2045
|
+
"typescript-eslint": "^8.18.2",
|
|
2046
|
+
vitest: "^2.1.8"
|
|
2047
|
+
},
|
|
2048
|
+
engines: {
|
|
2049
|
+
node: ">=18.0.0"
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
// src/main.ts
|
|
2054
|
+
function parseArgs() {
|
|
2055
|
+
const args = process.argv.slice(2);
|
|
2056
|
+
let help = false;
|
|
2057
|
+
let version = false;
|
|
2058
|
+
let once = false;
|
|
2059
|
+
let install = false;
|
|
2060
|
+
let uninstall = false;
|
|
2061
|
+
let force = false;
|
|
2062
|
+
let configPath;
|
|
2063
|
+
let runner;
|
|
2064
|
+
for (let i = 0;i < args.length; i++) {
|
|
2065
|
+
const arg = args[i];
|
|
2066
|
+
if (arg === "--help" || arg === "-h") {
|
|
2067
|
+
help = true;
|
|
2068
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
2069
|
+
version = true;
|
|
2070
|
+
} else if (arg === "--once") {
|
|
2071
|
+
once = true;
|
|
2072
|
+
} else if (arg === "--install") {
|
|
2073
|
+
install = true;
|
|
2074
|
+
} else if (arg === "--uninstall") {
|
|
2075
|
+
uninstall = true;
|
|
2076
|
+
} else if (arg === "--force") {
|
|
2077
|
+
force = true;
|
|
2078
|
+
} else if (arg === "--config" && i + 1 < args.length) {
|
|
2079
|
+
configPath = args[i + 1];
|
|
2080
|
+
i++;
|
|
2081
|
+
} else if (arg === "--runner" && i + 1 < args.length) {
|
|
2082
|
+
const nextArg = args[i + 1];
|
|
2083
|
+
if (nextArg === "npx" || nextArg === "bunx") {
|
|
2084
|
+
runner = nextArg;
|
|
2085
|
+
}
|
|
2086
|
+
i++;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return { help, version, once, install, uninstall, force, configPath, runner };
|
|
2090
|
+
}
|
|
2091
|
+
function showHelp() {
|
|
2092
|
+
console.log(`
|
|
2093
|
+
cc-api-statusline — Claude API statusline widget
|
|
2094
|
+
|
|
2095
|
+
Usage:
|
|
2096
|
+
cc-api-statusline [options]
|
|
2097
|
+
|
|
2098
|
+
Options:
|
|
2099
|
+
--help, -h Show this help message
|
|
2100
|
+
--version, -v Show version
|
|
2101
|
+
--once Fetch once and exit (no polling)
|
|
2102
|
+
--config <path> Use custom config file
|
|
2103
|
+
--install Register as Claude Code statusline widget
|
|
2104
|
+
--uninstall Remove statusline widget registration
|
|
2105
|
+
--runner <runner> Package runner: npx or bunx (default: auto-detect)
|
|
2106
|
+
--force Force overwrite existing statusline configuration
|
|
2107
|
+
|
|
2108
|
+
Environment Variables:
|
|
2109
|
+
ANTHROPIC_BASE_URL API endpoint (required)
|
|
2110
|
+
ANTHROPIC_AUTH_TOKEN API key (required)
|
|
2111
|
+
CC_STATUSLINE_PROVIDER Override provider detection
|
|
2112
|
+
CC_STATUSLINE_POLL Override poll interval (seconds)
|
|
2113
|
+
CC_STATUSLINE_TIMEOUT Piped mode timeout (milliseconds, default 1000)
|
|
2114
|
+
DEBUG or CC_STATUSLINE_DEBUG Enable debug logging to ~/.claude/cc-api-statusline/debug.log
|
|
2115
|
+
|
|
2116
|
+
Config File:
|
|
2117
|
+
~/.claude/cc-api-statusline/config.json
|
|
2118
|
+
|
|
2119
|
+
Documentation:
|
|
2120
|
+
https://github.com/liafonx/cc-api-statusline
|
|
2121
|
+
`.trim());
|
|
2122
|
+
}
|
|
2123
|
+
function showVersion() {
|
|
2124
|
+
console.log(`cc-api-statusline v${package_default.version}`);
|
|
2125
|
+
}
|
|
2126
|
+
function discardStdin() {
|
|
2127
|
+
if (!process.stdin.isTTY) {
|
|
2128
|
+
process.stdin.resume();
|
|
2129
|
+
process.stdin.on("data", () => {});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
async function main() {
|
|
2133
|
+
const startTime = Date.now();
|
|
2134
|
+
logger.debug("=== cc-api-statusline execution started ===");
|
|
2135
|
+
logger.debug("Start time", { startTime, version: package_default.version });
|
|
2136
|
+
discardStdin();
|
|
2137
|
+
const args = parseArgs();
|
|
2138
|
+
logger.debug("Parsed arguments", { args });
|
|
2139
|
+
if (args.help) {
|
|
2140
|
+
showHelp();
|
|
2141
|
+
process.exit(0);
|
|
2142
|
+
}
|
|
2143
|
+
if (args.version) {
|
|
2144
|
+
showVersion();
|
|
2145
|
+
process.exit(0);
|
|
2146
|
+
}
|
|
2147
|
+
if (args.install) {
|
|
2148
|
+
const existing = getExistingStatusLine();
|
|
2149
|
+
if (existing && !args.force) {
|
|
2150
|
+
console.error("Error: statusLine is already configured in settings.json");
|
|
2151
|
+
console.error(`Current command: ${existing}`);
|
|
2152
|
+
console.error("Use --force to overwrite, or --uninstall to remove first.");
|
|
2153
|
+
process.exit(1);
|
|
2154
|
+
}
|
|
2155
|
+
const runner = args.runner ?? (isBunxAvailable() ? "bunx" : "npx");
|
|
2156
|
+
installStatusLine(runner);
|
|
2157
|
+
console.log("✓ Statusline installed successfully!");
|
|
2158
|
+
console.log(` Runner: ${runner}`);
|
|
2159
|
+
console.log(` Command: ${runner} -y cc-api-statusline@latest`);
|
|
2160
|
+
console.log(` Config: ~/.claude/settings.json`);
|
|
2161
|
+
process.exit(0);
|
|
2162
|
+
}
|
|
2163
|
+
if (args.uninstall) {
|
|
2164
|
+
const existing = getExistingStatusLine();
|
|
2165
|
+
if (!existing) {
|
|
2166
|
+
console.log("No statusLine configuration found in settings.json");
|
|
2167
|
+
process.exit(0);
|
|
2168
|
+
}
|
|
2169
|
+
uninstallStatusLine();
|
|
2170
|
+
console.log("✓ Statusline uninstalled successfully");
|
|
2171
|
+
console.log(" Removed statusLine from ~/.claude/settings.json");
|
|
2172
|
+
process.exit(0);
|
|
2173
|
+
}
|
|
2174
|
+
const isPiped = !process.stdin.isTTY;
|
|
2175
|
+
logger.debug("Mode detection", { isPiped, once: args.once });
|
|
2176
|
+
if (!isPiped && !args.once) {
|
|
2177
|
+
console.log("Interactive configuration mode coming soon.");
|
|
2178
|
+
console.log("Use --once for a single fetch, or configure as a Claude Code statusline command.");
|
|
2179
|
+
process.exit(0);
|
|
2180
|
+
}
|
|
2181
|
+
const env = readCurrentEnv();
|
|
2182
|
+
logger.debug("Environment loaded", {
|
|
2183
|
+
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
2184
|
+
hasToken: !!env.authToken,
|
|
2185
|
+
providerOverride: env.providerOverride,
|
|
2186
|
+
pollIntervalOverride: env.pollIntervalOverride
|
|
2187
|
+
});
|
|
2188
|
+
const envError = validateRequiredEnv(env);
|
|
2189
|
+
if (envError) {
|
|
2190
|
+
const errorOutput = renderError("missing-env", "without-cache");
|
|
2191
|
+
process.stdout.write(errorOutput);
|
|
2192
|
+
process.exit(1);
|
|
2193
|
+
}
|
|
2194
|
+
const baseUrl = env.baseUrl;
|
|
2195
|
+
const authToken = env.authToken;
|
|
2196
|
+
if (!baseUrl || !authToken) {
|
|
2197
|
+
process.exit(1);
|
|
2198
|
+
}
|
|
2199
|
+
const config = loadConfig(args.configPath);
|
|
2200
|
+
const configPath = getConfigPath(args.configPath);
|
|
2201
|
+
const configHash = computeConfigHash(configPath);
|
|
2202
|
+
logger.debug("Config loaded", { configPath, configHash });
|
|
2203
|
+
const providerId = resolveProvider(baseUrl, env.providerOverride, config.customProviders ?? {});
|
|
2204
|
+
const provider = getProvider(providerId, config.customProviders ?? {});
|
|
2205
|
+
logger.debug("Provider resolved", { providerId });
|
|
2206
|
+
if (!provider) {
|
|
2207
|
+
logger.error("Provider not found", { providerId });
|
|
2208
|
+
const errorOutput = renderError("provider-unknown", "without-cache");
|
|
2209
|
+
process.stdout.write(errorOutput);
|
|
2210
|
+
process.exit(1);
|
|
2211
|
+
}
|
|
2212
|
+
const cachedEntry = readCache(baseUrl);
|
|
2213
|
+
logger.debug("Cache read", {
|
|
2214
|
+
cacheHit: !!cachedEntry,
|
|
2215
|
+
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
2216
|
+
});
|
|
2217
|
+
const timeoutBudgetMs = isPiped ? Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) : 1e4;
|
|
2218
|
+
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? 800, timeoutBudgetMs - 100) : 1e4;
|
|
2219
|
+
const ctx = {
|
|
2220
|
+
env,
|
|
2221
|
+
config,
|
|
2222
|
+
configHash,
|
|
2223
|
+
cachedEntry,
|
|
2224
|
+
providerId,
|
|
2225
|
+
provider,
|
|
2226
|
+
timeoutBudgetMs,
|
|
2227
|
+
startTime,
|
|
2228
|
+
fetchTimeoutMs
|
|
2229
|
+
};
|
|
2230
|
+
logger.debug("Execution context prepared", { timeoutBudgetMs, fetchTimeoutMs });
|
|
2231
|
+
const result = await executeCycle(ctx);
|
|
2232
|
+
const executionTime = Date.now() - startTime;
|
|
2233
|
+
logger.debug("Execution completed", {
|
|
2234
|
+
exitCode: result.exitCode,
|
|
2235
|
+
executionTime: `${executionTime}ms`,
|
|
2236
|
+
outputLength: result.output.length,
|
|
2237
|
+
cacheUpdate: !!result.cacheUpdate
|
|
2238
|
+
});
|
|
2239
|
+
if (isPiped) {
|
|
2240
|
+
const formatted = "\x1B[0m" + result.output.replace(/ /g, " ");
|
|
2241
|
+
process.stdout.write(formatted);
|
|
2242
|
+
logger.debug("Output formatted for piped mode (ANSI reset + NBSP)");
|
|
2243
|
+
} else {
|
|
2244
|
+
process.stdout.write(result.output);
|
|
2245
|
+
logger.debug("Output written (TTY mode)");
|
|
2246
|
+
}
|
|
2247
|
+
if (result.cacheUpdate) {
|
|
2248
|
+
writeCache(baseUrl, result.cacheUpdate);
|
|
2249
|
+
logger.debug("Cache updated");
|
|
2250
|
+
}
|
|
2251
|
+
logger.debug("=== Execution finished ===", { exitCode: result.exitCode });
|
|
2252
|
+
process.exit(result.exitCode);
|
|
2253
|
+
}
|
|
2254
|
+
main().catch((error) => {
|
|
2255
|
+
console.error("Fatal error:", error);
|
|
2256
|
+
process.exit(1);
|
|
2257
|
+
});
|