cc-api-statusline 0.1.4 → 0.1.9
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 +31 -9
- package/dist/cc-api-statusline.js +1143 -863
- package/package.json +1 -1
|
@@ -1,6 +1,141 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
// package.json
|
|
5
|
+
var package_default = {
|
|
6
|
+
name: "cc-api-statusline",
|
|
7
|
+
version: "0.1.9",
|
|
8
|
+
description: "Claude Code statusline tool that polls API usage from third-party proxy backends",
|
|
9
|
+
type: "module",
|
|
10
|
+
bin: {
|
|
11
|
+
"cc-api-statusline": "dist/cc-api-statusline.js"
|
|
12
|
+
},
|
|
13
|
+
scripts: {
|
|
14
|
+
start: "bun run src/main.ts --once",
|
|
15
|
+
dev: "bun run src/main.ts",
|
|
16
|
+
example: "cat docs/fixtures/ccstatusline-context.sample.json | bun run src/main.ts",
|
|
17
|
+
test: "bun run build && vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
lint: "eslint src",
|
|
20
|
+
build: "bun build src/main.ts --target=node --outfile=dist/cc-api-statusline.js",
|
|
21
|
+
check: "bun run test && bun run lint",
|
|
22
|
+
prepublishOnly: "bun run check"
|
|
23
|
+
},
|
|
24
|
+
files: [
|
|
25
|
+
"dist/"
|
|
26
|
+
],
|
|
27
|
+
keywords: [
|
|
28
|
+
"claude",
|
|
29
|
+
"statusline",
|
|
30
|
+
"api",
|
|
31
|
+
"usage",
|
|
32
|
+
"monitoring",
|
|
33
|
+
"tui",
|
|
34
|
+
"cli"
|
|
35
|
+
],
|
|
36
|
+
author: "Liafonx",
|
|
37
|
+
license: "MIT",
|
|
38
|
+
repository: {
|
|
39
|
+
type: "git",
|
|
40
|
+
url: "git+https://github.com/liafonx/cc-api-statusline.git"
|
|
41
|
+
},
|
|
42
|
+
homepage: "https://github.com/liafonx/cc-api-statusline#readme",
|
|
43
|
+
bugs: {
|
|
44
|
+
url: "https://github.com/liafonx/cc-api-statusline/issues"
|
|
45
|
+
},
|
|
46
|
+
dependencies: {},
|
|
47
|
+
devDependencies: {
|
|
48
|
+
"@eslint/js": "^9.17.0",
|
|
49
|
+
"@types/bun": "^1.1.14",
|
|
50
|
+
eslint: "^9.17.0",
|
|
51
|
+
typescript: "^5.7.2",
|
|
52
|
+
"typescript-eslint": "^8.18.2",
|
|
53
|
+
vitest: "^2.1.8"
|
|
54
|
+
},
|
|
55
|
+
engines: {
|
|
56
|
+
node: ">=18.0.0"
|
|
57
|
+
},
|
|
58
|
+
publishConfig: {
|
|
59
|
+
provenance: true
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/cli/args.ts
|
|
64
|
+
function parseArgs() {
|
|
65
|
+
const args = process.argv.slice(2);
|
|
66
|
+
let help = false;
|
|
67
|
+
let version = false;
|
|
68
|
+
let once = false;
|
|
69
|
+
let install = false;
|
|
70
|
+
let uninstall = false;
|
|
71
|
+
let force = false;
|
|
72
|
+
let configPath;
|
|
73
|
+
let runner;
|
|
74
|
+
for (let i = 0;i < args.length; i++) {
|
|
75
|
+
const arg = args[i];
|
|
76
|
+
if (arg === "--help" || arg === "-h") {
|
|
77
|
+
help = true;
|
|
78
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
79
|
+
version = true;
|
|
80
|
+
} else if (arg === "--once") {
|
|
81
|
+
once = true;
|
|
82
|
+
} else if (arg === "--install") {
|
|
83
|
+
install = true;
|
|
84
|
+
} else if (arg === "--uninstall") {
|
|
85
|
+
uninstall = true;
|
|
86
|
+
} else if (arg === "--force") {
|
|
87
|
+
force = true;
|
|
88
|
+
} else if (arg === "--config" && i + 1 < args.length) {
|
|
89
|
+
configPath = args[i + 1];
|
|
90
|
+
i++;
|
|
91
|
+
} else if (arg === "--runner" && i + 1 < args.length) {
|
|
92
|
+
const nextArg = args[i + 1];
|
|
93
|
+
if (nextArg === "npx" || nextArg === "bunx") {
|
|
94
|
+
runner = nextArg;
|
|
95
|
+
}
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { help, version, once, install, uninstall, force, configPath, runner };
|
|
100
|
+
}
|
|
101
|
+
function showHelp() {
|
|
102
|
+
console.log(`
|
|
103
|
+
cc-api-statusline — Claude API statusline widget
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
cc-api-statusline [options]
|
|
107
|
+
|
|
108
|
+
Options:
|
|
109
|
+
--help, -h Show this help message
|
|
110
|
+
--version, -v Show version
|
|
111
|
+
--once Fetch once and exit (no polling)
|
|
112
|
+
--config <path> Use custom config file
|
|
113
|
+
--install Register as Claude Code statusline widget
|
|
114
|
+
--uninstall Remove statusline widget registration
|
|
115
|
+
--runner <runner> Package runner: npx or bunx (default: auto-detect)
|
|
116
|
+
--force Force overwrite existing statusline configuration
|
|
117
|
+
|
|
118
|
+
Environment Variables:
|
|
119
|
+
ANTHROPIC_BASE_URL API endpoint (required)
|
|
120
|
+
ANTHROPIC_AUTH_TOKEN API key (required)
|
|
121
|
+
CC_STATUSLINE_PROVIDER Override provider detection
|
|
122
|
+
CC_STATUSLINE_POLL Override poll interval (seconds)
|
|
123
|
+
CC_STATUSLINE_TIMEOUT Piped mode timeout (milliseconds, default 1000)
|
|
124
|
+
DEBUG or CC_STATUSLINE_DEBUG Enable debug logging to ~/.claude/cc-api-statusline/debug.log
|
|
125
|
+
|
|
126
|
+
Config File:
|
|
127
|
+
~/.claude/cc-api-statusline/config.json
|
|
128
|
+
|
|
129
|
+
Documentation:
|
|
130
|
+
https://github.com/liafonx/cc-api-statusline
|
|
131
|
+
`.trim());
|
|
132
|
+
}
|
|
133
|
+
function showVersion() {
|
|
134
|
+
console.log(`cc-api-statusline v${package_default.version}`);
|
|
135
|
+
}
|
|
136
|
+
// src/services/settings.ts
|
|
137
|
+
import { readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
|
|
138
|
+
import { execSync } from "child_process";
|
|
4
139
|
|
|
5
140
|
// src/services/env.ts
|
|
6
141
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -91,8 +226,131 @@ function validateRequiredEnv(env) {
|
|
|
91
226
|
return null;
|
|
92
227
|
}
|
|
93
228
|
|
|
229
|
+
// src/services/atomic-write.ts
|
|
230
|
+
import { writeFileSync, renameSync, unlinkSync, existsSync as existsSync3, chmodSync } from "fs";
|
|
231
|
+
import { dirname } from "path";
|
|
232
|
+
|
|
233
|
+
// src/services/ensure-dir.ts
|
|
234
|
+
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
235
|
+
function ensureDir(dirPath) {
|
|
236
|
+
if (!existsSync2(dirPath)) {
|
|
237
|
+
mkdirSync(dirPath, { recursive: true, mode: 448 });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/services/atomic-write.ts
|
|
242
|
+
function atomicWriteFile(filePath, content, opts = {}) {
|
|
243
|
+
const { mode = 384, ensureParentDir: ensureParent = false, appendNewline = false } = opts;
|
|
244
|
+
const nonce = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
245
|
+
const tmpPath = `${filePath}.${nonce}.tmp`;
|
|
246
|
+
try {
|
|
247
|
+
if (ensureParent) {
|
|
248
|
+
const dir = dirname(filePath);
|
|
249
|
+
ensureDir(dir);
|
|
250
|
+
}
|
|
251
|
+
const finalContent = appendNewline ? `${content}
|
|
252
|
+
` : content;
|
|
253
|
+
writeFileSync(tmpPath, finalContent, { encoding: "utf-8", mode });
|
|
254
|
+
try {
|
|
255
|
+
chmodSync(tmpPath, mode);
|
|
256
|
+
} catch {}
|
|
257
|
+
renameSync(tmpPath, filePath);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
try {
|
|
260
|
+
if (existsSync3(tmpPath)) {
|
|
261
|
+
unlinkSync(tmpPath);
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
throw new Error(`Failed to write file atomically: ${error}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/services/settings.ts
|
|
269
|
+
function loadClaudeSettings() {
|
|
270
|
+
const path = getSettingsJsonPath();
|
|
271
|
+
if (!existsSync4(path)) {
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const content = readFileSync2(path, "utf-8");
|
|
276
|
+
return JSON.parse(content);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.warn(`Failed to read settings from ${path}: ${error}`);
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function saveClaudeSettings(settings) {
|
|
283
|
+
const path = getSettingsJsonPath();
|
|
284
|
+
try {
|
|
285
|
+
const content = JSON.stringify(settings, null, 2);
|
|
286
|
+
atomicWriteFile(path, content, { ensureParentDir: true, appendNewline: true });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error(`Failed to write settings to ${path}: ${error}`);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function getExistingStatusLine() {
|
|
293
|
+
const settings = loadClaudeSettings();
|
|
294
|
+
return settings.statusLine?.command ?? null;
|
|
295
|
+
}
|
|
296
|
+
function isBunxAvailable() {
|
|
297
|
+
try {
|
|
298
|
+
execSync("which bunx", { stdio: "ignore" });
|
|
299
|
+
return true;
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function installStatusLine(runner) {
|
|
305
|
+
const settings = loadClaudeSettings();
|
|
306
|
+
const updatedSettings = {
|
|
307
|
+
...settings,
|
|
308
|
+
statusLine: {
|
|
309
|
+
type: "command",
|
|
310
|
+
command: `${runner} -y cc-api-statusline@latest`,
|
|
311
|
+
padding: 0
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
saveClaudeSettings(updatedSettings);
|
|
315
|
+
}
|
|
316
|
+
function uninstallStatusLine() {
|
|
317
|
+
const settings = loadClaudeSettings();
|
|
318
|
+
if ("statusLine" in settings) {
|
|
319
|
+
const { statusLine: _, ...rest } = settings;
|
|
320
|
+
saveClaudeSettings(rest);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/cli/commands.ts
|
|
325
|
+
function handleInstall(args) {
|
|
326
|
+
const existing = getExistingStatusLine();
|
|
327
|
+
if (existing && !args.force) {
|
|
328
|
+
console.error("Error: statusLine is already configured in settings.json");
|
|
329
|
+
console.error(`Current command: ${existing}`);
|
|
330
|
+
console.error("Use --force to overwrite, or --uninstall to remove first.");
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
const runner = args.runner ?? (isBunxAvailable() ? "bunx" : "npx");
|
|
334
|
+
installStatusLine(runner);
|
|
335
|
+
console.log("✓ Statusline installed successfully!");
|
|
336
|
+
console.log(` Runner: ${runner}`);
|
|
337
|
+
console.log(` Command: ${runner} -y cc-api-statusline@latest`);
|
|
338
|
+
console.log(` Config: ~/.claude/settings.json`);
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
function handleUninstall() {
|
|
342
|
+
const existing = getExistingStatusLine();
|
|
343
|
+
if (!existing) {
|
|
344
|
+
console.log("No statusLine configuration found in settings.json");
|
|
345
|
+
process.exit(0);
|
|
346
|
+
}
|
|
347
|
+
uninstallStatusLine();
|
|
348
|
+
console.log("✓ Statusline uninstalled successfully");
|
|
349
|
+
console.log(" Removed statusLine from ~/.claude/settings.json");
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
94
352
|
// src/services/cache.ts
|
|
95
|
-
import { readFileSync as
|
|
353
|
+
import { readFileSync as readFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
96
354
|
import { join as join2 } from "path";
|
|
97
355
|
import { homedir as homedir2 } from "os";
|
|
98
356
|
|
|
@@ -123,19 +381,22 @@ function computeSoonestReset(usage) {
|
|
|
123
381
|
times.push(usage.monthly.resetsAt);
|
|
124
382
|
if (times.length === 0)
|
|
125
383
|
return null;
|
|
126
|
-
times.sort();
|
|
127
|
-
return
|
|
384
|
+
const sorted = [...times].sort();
|
|
385
|
+
return sorted[0] ?? null;
|
|
128
386
|
}
|
|
129
387
|
// src/types/config.ts
|
|
130
388
|
var DEFAULT_CONFIG = {
|
|
131
389
|
display: {
|
|
132
390
|
layout: "standard",
|
|
133
|
-
displayMode: "
|
|
391
|
+
displayMode: "text",
|
|
392
|
+
progressStyle: "icon",
|
|
134
393
|
barSize: "medium",
|
|
135
|
-
barStyle: "
|
|
394
|
+
barStyle: "block",
|
|
136
395
|
separator: " | ",
|
|
137
396
|
maxWidth: 100,
|
|
138
|
-
clockFormat: "24h"
|
|
397
|
+
clockFormat: "24h",
|
|
398
|
+
colorMode: "auto",
|
|
399
|
+
nerdFont: "auto"
|
|
139
400
|
},
|
|
140
401
|
components: {
|
|
141
402
|
daily: true,
|
|
@@ -144,7 +405,8 @@ var DEFAULT_CONFIG = {
|
|
|
144
405
|
balance: true,
|
|
145
406
|
tokens: false,
|
|
146
407
|
rateLimit: false,
|
|
147
|
-
plan: false
|
|
408
|
+
plan: false,
|
|
409
|
+
divider: true
|
|
148
410
|
},
|
|
149
411
|
colors: {
|
|
150
412
|
auto: {
|
|
@@ -201,6 +463,33 @@ var COMPONENT_FULL_LABELS = {
|
|
|
201
463
|
rateLimit: "Rate",
|
|
202
464
|
plan: "Plan"
|
|
203
465
|
};
|
|
466
|
+
var COMPONENT_EMOJI_LABELS = {
|
|
467
|
+
daily: "\uD83D\uDCC5",
|
|
468
|
+
weekly: "\uD83D\uDCC6",
|
|
469
|
+
monthly: "\uD83D\uDDD3️",
|
|
470
|
+
balance: "\uD83D\uDCB0",
|
|
471
|
+
tokens: "\uD83D\uDD22",
|
|
472
|
+
rateLimit: "⚡",
|
|
473
|
+
plan: "\uD83D\uDCCB"
|
|
474
|
+
};
|
|
475
|
+
var COMPONENT_NERD_LABELS = {
|
|
476
|
+
daily: "",
|
|
477
|
+
weekly: "",
|
|
478
|
+
monthly: "",
|
|
479
|
+
balance: "",
|
|
480
|
+
tokens: "",
|
|
481
|
+
rateLimit: "",
|
|
482
|
+
plan: ""
|
|
483
|
+
};
|
|
484
|
+
var DEFAULT_COMPONENT_ORDER = [
|
|
485
|
+
"daily",
|
|
486
|
+
"weekly",
|
|
487
|
+
"monthly",
|
|
488
|
+
"balance",
|
|
489
|
+
"tokens",
|
|
490
|
+
"rateLimit",
|
|
491
|
+
"plan"
|
|
492
|
+
];
|
|
204
493
|
// src/types/cache.ts
|
|
205
494
|
var CACHE_VERSION = 1;
|
|
206
495
|
function isCacheEntry(value) {
|
|
@@ -226,9 +515,7 @@ function getCacheDir() {
|
|
|
226
515
|
}
|
|
227
516
|
function ensureCacheDir() {
|
|
228
517
|
const dir = getCacheDir();
|
|
229
|
-
|
|
230
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
231
|
-
}
|
|
518
|
+
ensureDir(dir);
|
|
232
519
|
}
|
|
233
520
|
function getCachePath(baseUrl) {
|
|
234
521
|
const hash = shortHash(baseUrl, 12);
|
|
@@ -236,11 +523,11 @@ function getCachePath(baseUrl) {
|
|
|
236
523
|
}
|
|
237
524
|
function readCache(baseUrl) {
|
|
238
525
|
const path = getCachePath(baseUrl);
|
|
239
|
-
if (!
|
|
526
|
+
if (!existsSync5(path)) {
|
|
240
527
|
return null;
|
|
241
528
|
}
|
|
242
529
|
try {
|
|
243
|
-
const content =
|
|
530
|
+
const content = readFileSync3(path, "utf-8");
|
|
244
531
|
const data = JSON.parse(content);
|
|
245
532
|
if (!isCacheEntry(data)) {
|
|
246
533
|
console.warn(`Invalid cache structure at ${path}`);
|
|
@@ -254,22 +541,12 @@ function readCache(baseUrl) {
|
|
|
254
541
|
}
|
|
255
542
|
function writeCache(baseUrl, entry) {
|
|
256
543
|
const path = getCachePath(baseUrl);
|
|
257
|
-
const tmpPath = `${path}.tmp`;
|
|
258
544
|
try {
|
|
259
545
|
ensureCacheDir();
|
|
260
546
|
const content = JSON.stringify(entry, null, 2);
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
chmodSync(tmpPath, 384);
|
|
264
|
-
} catch {}
|
|
265
|
-
renameSync(tmpPath, path);
|
|
547
|
+
atomicWriteFile(path, content);
|
|
266
548
|
} catch (error) {
|
|
267
549
|
console.warn(`Failed to write cache to ${path}: ${error}`);
|
|
268
|
-
try {
|
|
269
|
-
if (existsSync2(tmpPath)) {
|
|
270
|
-
unlinkSync(tmpPath);
|
|
271
|
-
}
|
|
272
|
-
} catch {}
|
|
273
550
|
}
|
|
274
551
|
}
|
|
275
552
|
function isCacheValid(entry, currentEnv) {
|
|
@@ -298,11 +575,11 @@ function isCacheRenderedLineUsable(entry, currentConfigHash) {
|
|
|
298
575
|
return entry.configHash === currentConfigHash;
|
|
299
576
|
}
|
|
300
577
|
function computeConfigHash(configPath) {
|
|
301
|
-
if (!
|
|
578
|
+
if (!existsSync5(configPath)) {
|
|
302
579
|
return sha256("").slice(0, 12);
|
|
303
580
|
}
|
|
304
581
|
try {
|
|
305
|
-
const bytes =
|
|
582
|
+
const bytes = readFileSync3(configPath);
|
|
306
583
|
return shortHash(bytes.toString("utf-8"), 12);
|
|
307
584
|
} catch (error) {
|
|
308
585
|
console.warn(`Failed to read config for hash: ${error}`);
|
|
@@ -323,11 +600,11 @@ function getProviderDetectionCachePath(baseUrl) {
|
|
|
323
600
|
}
|
|
324
601
|
function readProviderDetectionCache(baseUrl) {
|
|
325
602
|
const path = getProviderDetectionCachePath(baseUrl);
|
|
326
|
-
if (!
|
|
603
|
+
if (!existsSync5(path)) {
|
|
327
604
|
return null;
|
|
328
605
|
}
|
|
329
606
|
try {
|
|
330
|
-
const content =
|
|
607
|
+
const content = readFileSync3(path, "utf-8");
|
|
331
608
|
const data = JSON.parse(content);
|
|
332
609
|
if (!isProviderDetectionCacheEntry(data)) {
|
|
333
610
|
console.warn(`Invalid provider detection cache structure at ${path}`);
|
|
@@ -339,7 +616,7 @@ function readProviderDetectionCache(baseUrl) {
|
|
|
339
616
|
const ttlMs = data.ttlSeconds * 1000;
|
|
340
617
|
if (age >= ttlMs) {
|
|
341
618
|
try {
|
|
342
|
-
|
|
619
|
+
unlinkSync2(path);
|
|
343
620
|
} catch {}
|
|
344
621
|
return null;
|
|
345
622
|
}
|
|
@@ -351,27 +628,17 @@ function readProviderDetectionCache(baseUrl) {
|
|
|
351
628
|
}
|
|
352
629
|
function writeProviderDetectionCache(baseUrl, entry) {
|
|
353
630
|
const path = getProviderDetectionCachePath(baseUrl);
|
|
354
|
-
const tmpPath = `${path}.tmp`;
|
|
355
631
|
try {
|
|
356
632
|
ensureCacheDir();
|
|
357
633
|
const content = JSON.stringify(entry, null, 2);
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
chmodSync(tmpPath, 384);
|
|
361
|
-
} catch {}
|
|
362
|
-
renameSync(tmpPath, path);
|
|
634
|
+
atomicWriteFile(path, content);
|
|
363
635
|
} catch (error) {
|
|
364
636
|
console.warn(`Failed to write provider detection cache to ${path}: ${error}`);
|
|
365
|
-
try {
|
|
366
|
-
if (existsSync2(tmpPath)) {
|
|
367
|
-
unlinkSync(tmpPath);
|
|
368
|
-
}
|
|
369
|
-
} catch {}
|
|
370
637
|
}
|
|
371
638
|
}
|
|
372
639
|
|
|
373
640
|
// src/services/config.ts
|
|
374
|
-
import { readFileSync as
|
|
641
|
+
import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
|
|
375
642
|
import { join as join3 } from "path";
|
|
376
643
|
import { homedir as homedir3 } from "os";
|
|
377
644
|
function getConfigDir() {
|
|
@@ -397,31 +664,42 @@ function deepMerge(target, source) {
|
|
|
397
664
|
return result;
|
|
398
665
|
}
|
|
399
666
|
function validateConfig(config) {
|
|
400
|
-
|
|
667
|
+
let maxWidth = config.display.maxWidth;
|
|
668
|
+
let pollIntervalSeconds = config.pollIntervalSeconds;
|
|
669
|
+
let pipedRequestTimeoutMs = config.pipedRequestTimeoutMs;
|
|
670
|
+
if (maxWidth < 20) {
|
|
401
671
|
console.warn("Warning: display.maxWidth < 20, clamping to 20");
|
|
402
|
-
|
|
672
|
+
maxWidth = 20;
|
|
403
673
|
}
|
|
404
|
-
if (
|
|
674
|
+
if (maxWidth > 100) {
|
|
405
675
|
console.warn("Warning: display.maxWidth > 100, clamping to 100");
|
|
406
|
-
|
|
676
|
+
maxWidth = 100;
|
|
407
677
|
}
|
|
408
|
-
if (
|
|
678
|
+
if (pollIntervalSeconds !== undefined && pollIntervalSeconds < 5) {
|
|
409
679
|
console.warn("Warning: pollIntervalSeconds < 5, clamping to 5");
|
|
410
|
-
|
|
680
|
+
pollIntervalSeconds = 5;
|
|
411
681
|
}
|
|
412
|
-
if (
|
|
682
|
+
if (pipedRequestTimeoutMs !== undefined && pipedRequestTimeoutMs < 100) {
|
|
413
683
|
console.warn("Warning: pipedRequestTimeoutMs < 100, clamping to 100");
|
|
414
|
-
|
|
684
|
+
pipedRequestTimeoutMs = 100;
|
|
415
685
|
}
|
|
416
|
-
return
|
|
686
|
+
return {
|
|
687
|
+
...config,
|
|
688
|
+
display: {
|
|
689
|
+
...config.display,
|
|
690
|
+
maxWidth
|
|
691
|
+
},
|
|
692
|
+
pollIntervalSeconds,
|
|
693
|
+
pipedRequestTimeoutMs
|
|
694
|
+
};
|
|
417
695
|
}
|
|
418
696
|
function loadConfig(configPath) {
|
|
419
697
|
const path = getConfigPath(configPath);
|
|
420
|
-
if (!
|
|
698
|
+
if (!existsSync6(path)) {
|
|
421
699
|
return DEFAULT_CONFIG;
|
|
422
700
|
}
|
|
423
701
|
try {
|
|
424
|
-
const content =
|
|
702
|
+
const content = readFileSync4(path, "utf-8");
|
|
425
703
|
const userConfig = JSON.parse(content);
|
|
426
704
|
const merged = deepMerge(DEFAULT_CONFIG, userConfig);
|
|
427
705
|
return validateConfig(merged);
|
|
@@ -451,43 +729,12 @@ class TimeoutError extends Error {
|
|
|
451
729
|
}
|
|
452
730
|
}
|
|
453
731
|
|
|
454
|
-
class RedirectError extends Error {
|
|
455
|
-
constructor(message) {
|
|
456
|
-
super(message);
|
|
457
|
-
this.name = "RedirectError";
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
732
|
class ResponseTooLargeError extends Error {
|
|
462
733
|
constructor(message = "Response body exceeds 1MB limit") {
|
|
463
734
|
super(message);
|
|
464
735
|
this.name = "ResponseTooLargeError";
|
|
465
736
|
}
|
|
466
737
|
}
|
|
467
|
-
function isSecureUrl(url) {
|
|
468
|
-
try {
|
|
469
|
-
const parsed = new URL(url);
|
|
470
|
-
if (parsed.protocol === "https:") {
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
if (parsed.protocol === "http:") {
|
|
474
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
475
|
-
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") {
|
|
476
|
-
return true;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return false;
|
|
480
|
-
} catch {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
function getHostname(url) {
|
|
485
|
-
try {
|
|
486
|
-
return new URL(url).hostname.toLowerCase();
|
|
487
|
-
} catch {
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
738
|
async function readBodyWithLimit(response) {
|
|
492
739
|
const MAX_SIZE = 1024 * 1024;
|
|
493
740
|
let bytesRead = 0;
|
|
@@ -528,17 +775,10 @@ async function readBodyWithLimit(response) {
|
|
|
528
775
|
}
|
|
529
776
|
}
|
|
530
777
|
async function secureFetch(url, options = {}, timeoutMs = 5000, userAgent) {
|
|
531
|
-
if (!isSecureUrl(url)) {
|
|
532
|
-
throw new HttpError(`Insecure URL rejected (must be HTTPS or localhost): ${url}`);
|
|
533
|
-
}
|
|
534
|
-
const originalHostname = getHostname(url);
|
|
535
|
-
if (!originalHostname) {
|
|
536
|
-
throw new HttpError(`Invalid URL: ${url}`);
|
|
537
|
-
}
|
|
538
778
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
539
779
|
const fetchOptions = {
|
|
540
780
|
...options,
|
|
541
|
-
redirect: "
|
|
781
|
+
redirect: "follow",
|
|
542
782
|
signal
|
|
543
783
|
};
|
|
544
784
|
if (userAgent) {
|
|
@@ -548,17 +788,6 @@ async function secureFetch(url, options = {}, timeoutMs = 5000, userAgent) {
|
|
|
548
788
|
}
|
|
549
789
|
try {
|
|
550
790
|
const response = await fetch(url, fetchOptions);
|
|
551
|
-
if (response.status >= 300 && response.status < 400) {
|
|
552
|
-
const location = response.headers.get("Location");
|
|
553
|
-
if (location) {
|
|
554
|
-
const redirectHostname = getHostname(location);
|
|
555
|
-
if (redirectHostname && redirectHostname !== originalHostname) {
|
|
556
|
-
throw new RedirectError(`Cross-domain redirect blocked: ${originalHostname} → ${redirectHostname}`);
|
|
557
|
-
}
|
|
558
|
-
throw new RedirectError(`Redirect detected to: ${location}`);
|
|
559
|
-
}
|
|
560
|
-
throw new RedirectError("Redirect detected but Location header missing");
|
|
561
|
-
}
|
|
562
791
|
if (!response.ok) {
|
|
563
792
|
let errorContext = response.statusText;
|
|
564
793
|
try {
|
|
@@ -575,7 +804,7 @@ async function secureFetch(url, options = {}, timeoutMs = 5000, userAgent) {
|
|
|
575
804
|
throw new TimeoutError(`Request timed out after ${timeoutMs}ms`);
|
|
576
805
|
}
|
|
577
806
|
}
|
|
578
|
-
if (error instanceof HttpError || error instanceof TimeoutError || error instanceof
|
|
807
|
+
if (error instanceof HttpError || error instanceof TimeoutError || error instanceof ResponseTooLargeError) {
|
|
579
808
|
throw error;
|
|
580
809
|
}
|
|
581
810
|
if (error instanceof Error && error.message.includes("timed out")) {
|
|
@@ -586,15 +815,14 @@ async function secureFetch(url, options = {}, timeoutMs = 5000, userAgent) {
|
|
|
586
815
|
}
|
|
587
816
|
|
|
588
817
|
// src/services/user-agent.ts
|
|
589
|
-
import { execSync } from "child_process";
|
|
818
|
+
import { execSync as execSync2 } from "child_process";
|
|
590
819
|
import { join as join5 } from "path";
|
|
591
820
|
import { homedir as homedir5 } from "os";
|
|
592
821
|
|
|
593
822
|
// src/services/logger.ts
|
|
594
|
-
import { appendFileSync
|
|
595
|
-
import { join as join4, dirname } from "path";
|
|
823
|
+
import { appendFileSync } from "fs";
|
|
824
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
596
825
|
import { homedir as homedir4 } from "os";
|
|
597
|
-
|
|
598
826
|
class Logger {
|
|
599
827
|
enabled;
|
|
600
828
|
logPath;
|
|
@@ -608,10 +836,8 @@ class Logger {
|
|
|
608
836
|
}
|
|
609
837
|
ensureLogDir() {
|
|
610
838
|
try {
|
|
611
|
-
const dir =
|
|
612
|
-
|
|
613
|
-
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
614
|
-
}
|
|
839
|
+
const dir = dirname2(this.logPath);
|
|
840
|
+
ensureDir(dir);
|
|
615
841
|
} catch {
|
|
616
842
|
this.enabled = false;
|
|
617
843
|
}
|
|
@@ -685,7 +911,7 @@ function detectClaudeVersion() {
|
|
|
685
911
|
return null;
|
|
686
912
|
}
|
|
687
913
|
const claudePath = join5(homedir5(), ".claude", "bin", "claude");
|
|
688
|
-
const result =
|
|
914
|
+
const result = execSync2(`"${claudePath}" --version`, {
|
|
689
915
|
encoding: "utf-8",
|
|
690
916
|
timeout: 1000,
|
|
691
917
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -697,25 +923,32 @@ function detectClaudeVersion() {
|
|
|
697
923
|
}
|
|
698
924
|
}
|
|
699
925
|
|
|
700
|
-
// src/
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0);
|
|
716
|
-
return nextMonth.toISOString();
|
|
926
|
+
// src/providers/quota-window.ts
|
|
927
|
+
function createQuotaWindow(used, limit, resetsAt) {
|
|
928
|
+
if (used === undefined) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
if (!limit || limit <= 0) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
const remaining = Math.max(0, limit - used);
|
|
935
|
+
return {
|
|
936
|
+
used,
|
|
937
|
+
limit,
|
|
938
|
+
remaining,
|
|
939
|
+
resetsAt
|
|
940
|
+
};
|
|
717
941
|
}
|
|
718
942
|
|
|
943
|
+
// src/core/constants.ts
|
|
944
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 5000;
|
|
945
|
+
var EXIT_BUFFER_MS = 50;
|
|
946
|
+
var STALENESS_THRESHOLD_MINUTES = 5;
|
|
947
|
+
var VERY_STALE_THRESHOLD_MINUTES = 30;
|
|
948
|
+
var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
949
|
+
var GC_MAX_CACHE_FILES = 20;
|
|
950
|
+
var GC_ORPHAN_TMP_AGE_MS = 60 * 60 * 1000;
|
|
951
|
+
|
|
719
952
|
// src/providers/sub2api.ts
|
|
720
953
|
function mapPeriodTokens(data) {
|
|
721
954
|
if (!data)
|
|
@@ -730,108 +963,66 @@ function mapPeriodTokens(data) {
|
|
|
730
963
|
cost: data.cost ?? 0
|
|
731
964
|
};
|
|
732
965
|
}
|
|
733
|
-
function
|
|
734
|
-
if (used === undefined)
|
|
735
|
-
return null;
|
|
736
|
-
if (limit === null || limit === undefined)
|
|
737
|
-
return null;
|
|
738
|
-
const remaining = Math.max(0, limit - used);
|
|
739
|
-
return {
|
|
740
|
-
used,
|
|
741
|
-
limit,
|
|
742
|
-
remaining,
|
|
743
|
-
resetsAt
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
async function fetchSub2api(baseUrl, token, config, timeoutMs = 5000) {
|
|
966
|
+
async function fetchSub2api(baseUrl, token, config, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
747
967
|
const url = `${baseUrl}/v1/usage`;
|
|
748
968
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
749
969
|
if (resolvedUA) {
|
|
750
970
|
logger.debug(`Using User-Agent: ${resolvedUA}`);
|
|
751
971
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
Accept: "application/json"
|
|
758
|
-
}
|
|
759
|
-
}, timeoutMs, resolvedUA);
|
|
760
|
-
const data = JSON.parse(responseText);
|
|
761
|
-
const hasSubscription = !!data.subscription;
|
|
762
|
-
const billingMode = hasSubscription ? "subscription" : "balance";
|
|
763
|
-
const result = {
|
|
764
|
-
provider: "sub2api",
|
|
765
|
-
billingMode,
|
|
766
|
-
planName: data.planName ?? "Unknown",
|
|
767
|
-
fetchedAt: new Date().toISOString(),
|
|
768
|
-
resetSemantics: "end-of-day",
|
|
769
|
-
daily: null,
|
|
770
|
-
weekly: null,
|
|
771
|
-
monthly: null,
|
|
772
|
-
balance: null,
|
|
773
|
-
resetsAt: null,
|
|
774
|
-
tokenStats: null,
|
|
775
|
-
rateLimit: null
|
|
776
|
-
};
|
|
777
|
-
if (billingMode === "balance") {
|
|
778
|
-
const remaining = data.remaining ?? 0;
|
|
779
|
-
if (remaining === -1) {
|
|
780
|
-
result.balance = {
|
|
781
|
-
remaining: -1,
|
|
782
|
-
initial: null,
|
|
783
|
-
unit: data.unit ?? "USD"
|
|
784
|
-
};
|
|
785
|
-
} else {
|
|
786
|
-
result.balance = {
|
|
787
|
-
remaining,
|
|
788
|
-
initial: null,
|
|
789
|
-
unit: data.unit ?? "USD"
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
} else {
|
|
793
|
-
const sub = data.subscription;
|
|
794
|
-
if (!sub) {
|
|
795
|
-
throw new Error("Subscription mode but no subscription object in response");
|
|
796
|
-
}
|
|
797
|
-
result.daily = createQuotaWindow(sub.daily_usage_usd, sub.daily_limit_usd, computeNextMidnightLocal());
|
|
798
|
-
result.weekly = createQuotaWindow(sub.weekly_usage_usd, sub.weekly_limit_usd, computeNextMondayLocal());
|
|
799
|
-
result.monthly = createQuotaWindow(sub.monthly_usage_usd, sub.monthly_limit_usd, computeFirstOfNextMonthLocal());
|
|
800
|
-
result.resetsAt = computeSoonestReset(result);
|
|
801
|
-
}
|
|
802
|
-
if (data.usage) {
|
|
803
|
-
result.tokenStats = {
|
|
804
|
-
today: mapPeriodTokens(data.usage.today),
|
|
805
|
-
total: mapPeriodTokens(data.usage.total),
|
|
806
|
-
rpm: data.usage.rpm ?? null,
|
|
807
|
-
tpm: data.usage.tpm ?? null
|
|
808
|
-
};
|
|
972
|
+
const responseText = await secureFetch(url, {
|
|
973
|
+
method: "GET",
|
|
974
|
+
headers: {
|
|
975
|
+
Authorization: `Bearer ${token}`,
|
|
976
|
+
Accept: "application/json"
|
|
809
977
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
978
|
+
}, timeoutMs, resolvedUA);
|
|
979
|
+
const data = JSON.parse(responseText);
|
|
980
|
+
if (!data || typeof data !== "object") {
|
|
981
|
+
throw new Error("Invalid response: expected object");
|
|
982
|
+
}
|
|
983
|
+
if (typeof data.planName !== "string" && data.planName !== undefined) {
|
|
984
|
+
throw new Error("Invalid response: planName must be string or undefined");
|
|
985
|
+
}
|
|
986
|
+
const hasSubscription = !!data.subscription;
|
|
987
|
+
const billingMode = hasSubscription ? "subscription" : "balance";
|
|
988
|
+
const base = createEmptyNormalizedUsage("sub2api", billingMode, data.planName ?? "Unknown");
|
|
989
|
+
let balance = null;
|
|
990
|
+
let daily = null;
|
|
991
|
+
let weekly = null;
|
|
992
|
+
let monthly = null;
|
|
993
|
+
let resetsAt = null;
|
|
994
|
+
if (billingMode === "balance") {
|
|
995
|
+
balance = {
|
|
996
|
+
remaining: data.remaining ?? 0,
|
|
997
|
+
initial: null,
|
|
998
|
+
unit: data.unit ?? "USD"
|
|
999
|
+
};
|
|
1000
|
+
} else {
|
|
1001
|
+
const sub = data.subscription;
|
|
1002
|
+
if (!sub) {
|
|
1003
|
+
throw new Error("Subscription mode but no subscription object in response");
|
|
832
1004
|
}
|
|
833
|
-
|
|
834
|
-
|
|
1005
|
+
daily = createQuotaWindow(sub.daily_usage_usd, sub.daily_limit_usd, null);
|
|
1006
|
+
weekly = createQuotaWindow(sub.weekly_usage_usd, sub.weekly_limit_usd, null);
|
|
1007
|
+
monthly = createQuotaWindow(sub.monthly_usage_usd, sub.monthly_limit_usd, null);
|
|
1008
|
+
const tempResult = { ...base, daily, weekly, monthly };
|
|
1009
|
+
resetsAt = computeSoonestReset(tempResult);
|
|
1010
|
+
}
|
|
1011
|
+
const tokenStats = data.usage ? {
|
|
1012
|
+
today: mapPeriodTokens(data.usage.today),
|
|
1013
|
+
total: mapPeriodTokens(data.usage.total),
|
|
1014
|
+
rpm: data.usage.rpm ?? null,
|
|
1015
|
+
tpm: data.usage.tpm ?? null
|
|
1016
|
+
} : null;
|
|
1017
|
+
return {
|
|
1018
|
+
...base,
|
|
1019
|
+
balance,
|
|
1020
|
+
daily,
|
|
1021
|
+
weekly,
|
|
1022
|
+
monthly,
|
|
1023
|
+
resetsAt,
|
|
1024
|
+
tokenStats
|
|
1025
|
+
};
|
|
835
1026
|
}
|
|
836
1027
|
|
|
837
1028
|
// src/providers/health-probe.ts
|
|
@@ -872,6 +1063,13 @@ async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
|
872
1063
|
}
|
|
873
1064
|
}
|
|
874
1065
|
|
|
1066
|
+
// src/services/time.ts
|
|
1067
|
+
function computeNextMidnightLocal() {
|
|
1068
|
+
const now = new Date;
|
|
1069
|
+
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0);
|
|
1070
|
+
return tomorrow.toISOString();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
875
1073
|
// src/providers/claude-relay-service.ts
|
|
876
1074
|
function computeWeeklyResetTime(resetDay, resetHour) {
|
|
877
1075
|
const now = new Date;
|
|
@@ -884,20 +1082,7 @@ function computeWeeklyResetTime(resetDay, resetHour) {
|
|
|
884
1082
|
const resetDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilReset, resetHour, 0, 0, 0));
|
|
885
1083
|
return resetDate.toISOString();
|
|
886
1084
|
}
|
|
887
|
-
function
|
|
888
|
-
if (used === undefined)
|
|
889
|
-
return null;
|
|
890
|
-
if (!limit || limit <= 0)
|
|
891
|
-
return null;
|
|
892
|
-
const remaining = Math.max(0, limit - used);
|
|
893
|
-
return {
|
|
894
|
-
used,
|
|
895
|
-
limit,
|
|
896
|
-
remaining,
|
|
897
|
-
resetsAt
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000) {
|
|
1085
|
+
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
901
1086
|
const origin = extractOrigin(baseUrl);
|
|
902
1087
|
const url = `${origin}/apiStats/api/user-stats`;
|
|
903
1088
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
@@ -913,69 +1098,65 @@ async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000)
|
|
|
913
1098
|
body: JSON.stringify({ apiKey: token })
|
|
914
1099
|
}, timeoutMs, resolvedUA);
|
|
915
1100
|
const response = JSON.parse(responseText);
|
|
1101
|
+
if (!response || typeof response !== "object") {
|
|
1102
|
+
throw new Error("Invalid response: expected object");
|
|
1103
|
+
}
|
|
916
1104
|
if (!response.success) {
|
|
917
1105
|
throw new HttpError("Relay API returned success: false");
|
|
918
1106
|
}
|
|
919
1107
|
const data = response.data;
|
|
1108
|
+
if (!data || typeof data !== "object") {
|
|
1109
|
+
throw new Error("Invalid response: missing data object");
|
|
1110
|
+
}
|
|
920
1111
|
const limits = data.limits;
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1112
|
+
if (!limits || typeof limits !== "object") {
|
|
1113
|
+
throw new Error("Invalid response: missing limits object");
|
|
1114
|
+
}
|
|
1115
|
+
const base = createEmptyNormalizedUsage("claude-relay-service", "subscription", data.name ?? "API Key");
|
|
1116
|
+
const daily = createQuotaWindow(limits.currentDailyCost, limits.dailyCostLimit, computeNextMidnightLocal());
|
|
1117
|
+
const weeklyResetsAt = limits.weeklyResetDay !== undefined && limits.weeklyResetHour !== undefined ? computeWeeklyResetTime(limits.weeklyResetDay, limits.weeklyResetHour) : null;
|
|
1118
|
+
const weeklyBase = createQuotaWindow(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, weeklyResetsAt);
|
|
1119
|
+
const weekly = weeklyBase ? { ...weeklyBase, qualifier: "Opus" } : null;
|
|
1120
|
+
const monthly = createQuotaWindow(limits.currentTotalCost, limits.totalCostLimit, null);
|
|
1121
|
+
const resetsAt = limits.windowEndTime ? new Date(limits.windowEndTime).toISOString() : (() => {
|
|
1122
|
+
const tempResult = { ...base, daily, weekly, monthly };
|
|
1123
|
+
return computeSoonestReset(tempResult);
|
|
1124
|
+
})();
|
|
1125
|
+
const tokenStats = data.usage?.total ? {
|
|
1126
|
+
today: null,
|
|
1127
|
+
total: {
|
|
1128
|
+
requests: data.usage.total.requests ?? 0,
|
|
1129
|
+
inputTokens: data.usage.total.inputTokens ?? 0,
|
|
1130
|
+
outputTokens: data.usage.total.outputTokens ?? 0,
|
|
1131
|
+
cacheCreationTokens: data.usage.total.cacheCreateTokens ?? 0,
|
|
1132
|
+
cacheReadTokens: data.usage.total.cacheReadTokens ?? 0,
|
|
1133
|
+
totalTokens: data.usage.total.tokens ?? (data.usage.total.inputTokens ?? 0) + (data.usage.total.outputTokens ?? 0),
|
|
1134
|
+
cost: data.usage.total.cost ?? 0
|
|
1135
|
+
},
|
|
1136
|
+
rpm: null,
|
|
1137
|
+
tpm: null
|
|
1138
|
+
} : null;
|
|
1139
|
+
const rateLimit = limits.rateLimitWindow !== undefined ? {
|
|
1140
|
+
windowSeconds: limits.rateLimitWindow * 60,
|
|
1141
|
+
requestsUsed: limits.currentWindowRequests ?? 0,
|
|
1142
|
+
requestsLimit: limits.rateLimitRequests && limits.rateLimitRequests > 0 ? limits.rateLimitRequests : null,
|
|
1143
|
+
costUsed: limits.currentWindowCost ?? 0,
|
|
1144
|
+
costLimit: limits.rateLimitCost && limits.rateLimitCost > 0 ? limits.rateLimitCost : null,
|
|
1145
|
+
remainingSeconds: limits.windowRemainingSeconds ?? 0
|
|
1146
|
+
} : null;
|
|
1147
|
+
return {
|
|
1148
|
+
...base,
|
|
926
1149
|
resetSemantics: "rolling-window",
|
|
927
|
-
daily
|
|
928
|
-
weekly
|
|
929
|
-
monthly
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
rateLimit: null
|
|
1150
|
+
daily,
|
|
1151
|
+
weekly,
|
|
1152
|
+
monthly,
|
|
1153
|
+
resetsAt,
|
|
1154
|
+
tokenStats,
|
|
1155
|
+
rateLimit
|
|
934
1156
|
};
|
|
935
|
-
result.daily = createQuotaWindow2(limits.currentDailyCost, limits.dailyCostLimit, computeNextMidnightLocal());
|
|
936
|
-
if (limits.weeklyResetDay !== undefined && limits.weeklyResetHour !== undefined) {
|
|
937
|
-
const weeklyResetsAt = computeWeeklyResetTime(limits.weeklyResetDay, limits.weeklyResetHour);
|
|
938
|
-
result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, weeklyResetsAt);
|
|
939
|
-
} else {
|
|
940
|
-
result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, computeNextMondayLocal());
|
|
941
|
-
}
|
|
942
|
-
result.monthly = null;
|
|
943
|
-
if (limits.windowEndTime) {
|
|
944
|
-
result.resetsAt = new Date(limits.windowEndTime).toISOString();
|
|
945
|
-
} else {
|
|
946
|
-
result.resetsAt = computeSoonestReset(result);
|
|
947
|
-
}
|
|
948
|
-
if (data.usage?.total) {
|
|
949
|
-
const total = data.usage.total;
|
|
950
|
-
result.tokenStats = {
|
|
951
|
-
today: null,
|
|
952
|
-
total: {
|
|
953
|
-
requests: total.requests ?? 0,
|
|
954
|
-
inputTokens: total.inputTokens ?? 0,
|
|
955
|
-
outputTokens: total.outputTokens ?? 0,
|
|
956
|
-
cacheCreationTokens: total.cacheCreateTokens ?? 0,
|
|
957
|
-
cacheReadTokens: total.cacheReadTokens ?? 0,
|
|
958
|
-
totalTokens: total.tokens ?? (total.inputTokens ?? 0) + (total.outputTokens ?? 0),
|
|
959
|
-
cost: total.cost ?? 0
|
|
960
|
-
},
|
|
961
|
-
rpm: null,
|
|
962
|
-
tpm: null
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
if (limits.rateLimitWindow !== undefined) {
|
|
966
|
-
result.rateLimit = {
|
|
967
|
-
windowSeconds: limits.rateLimitWindow * 60,
|
|
968
|
-
requestsUsed: limits.currentWindowRequests ?? 0,
|
|
969
|
-
requestsLimit: limits.rateLimitRequests && limits.rateLimitRequests > 0 ? limits.rateLimitRequests : null,
|
|
970
|
-
costUsed: limits.currentWindowCost ?? 0,
|
|
971
|
-
costLimit: limits.rateLimitCost && limits.rateLimitCost > 0 ? limits.rateLimitCost : null,
|
|
972
|
-
remainingSeconds: limits.windowRemainingSeconds ?? 0
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
return result;
|
|
976
1157
|
}
|
|
977
1158
|
|
|
978
|
-
// src/providers/custom.ts
|
|
1159
|
+
// src/providers/custom-mapping.ts
|
|
979
1160
|
function resolveJsonPath(data, path) {
|
|
980
1161
|
if (!path.startsWith("$.")) {
|
|
981
1162
|
return path;
|
|
@@ -1020,6 +1201,107 @@ function extractString(data, mapping, defaultValue = "") {
|
|
|
1020
1201
|
return value;
|
|
1021
1202
|
return String(value);
|
|
1022
1203
|
}
|
|
1204
|
+
function extractQuotaWindow(data, mapping, prefix) {
|
|
1205
|
+
const usedKey = `${prefix}.used`;
|
|
1206
|
+
const limitKey = `${prefix}.limit`;
|
|
1207
|
+
const resetsAtKey = `${prefix}.resetsAt`;
|
|
1208
|
+
const used = extractNumber(data, mapping[usedKey]);
|
|
1209
|
+
if (used === null) {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
const limitRaw = extractNumber(data, mapping[limitKey]);
|
|
1213
|
+
const limit = limitRaw === 0 ? null : limitRaw;
|
|
1214
|
+
return {
|
|
1215
|
+
used,
|
|
1216
|
+
limit,
|
|
1217
|
+
remaining: limit !== null ? Math.max(0, limit - used) : null,
|
|
1218
|
+
resetsAt: extractString(data, mapping[resetsAtKey], "") || null
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function extractTokenStatsPeriod(data, mapping, prefix) {
|
|
1222
|
+
const requestsKey = `${prefix}.requests`;
|
|
1223
|
+
const requests = extractNumber(data, mapping[requestsKey]);
|
|
1224
|
+
if (requests === null) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
requests,
|
|
1229
|
+
inputTokens: extractNumber(data, mapping[`${prefix}.inputTokens`]) ?? 0,
|
|
1230
|
+
outputTokens: extractNumber(data, mapping[`${prefix}.outputTokens`]) ?? 0,
|
|
1231
|
+
cacheCreationTokens: extractNumber(data, mapping[`${prefix}.cacheCreationTokens`]) ?? 0,
|
|
1232
|
+
cacheReadTokens: extractNumber(data, mapping[`${prefix}.cacheReadTokens`]) ?? 0,
|
|
1233
|
+
totalTokens: extractNumber(data, mapping[`${prefix}.totalTokens`]) ?? 0,
|
|
1234
|
+
cost: extractNumber(data, mapping[`${prefix}.cost`]) ?? 0
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function mapResponseToUsage(responseData, mapping, providerConfig) {
|
|
1238
|
+
const billingModeStr = extractString(responseData, mapping.billingMode, "subscription");
|
|
1239
|
+
const billingMode = billingModeStr === "balance" ? "balance" : "subscription";
|
|
1240
|
+
const planName = extractString(responseData, mapping.planName, providerConfig.displayName ?? providerConfig.id);
|
|
1241
|
+
const base = createEmptyNormalizedUsage(providerConfig.id, billingMode, planName);
|
|
1242
|
+
const balance = mapping["balance.remaining"] ? (() => {
|
|
1243
|
+
const remaining = extractNumber(responseData, mapping["balance.remaining"]);
|
|
1244
|
+
if (remaining === null)
|
|
1245
|
+
return null;
|
|
1246
|
+
return {
|
|
1247
|
+
remaining,
|
|
1248
|
+
initial: extractNumber(responseData, mapping["balance.initial"]),
|
|
1249
|
+
unit: extractString(responseData, mapping["balance.unit"], "USD")
|
|
1250
|
+
};
|
|
1251
|
+
})() : null;
|
|
1252
|
+
const daily = extractQuotaWindow(responseData, mapping, "daily");
|
|
1253
|
+
const weekly = extractQuotaWindow(responseData, mapping, "weekly");
|
|
1254
|
+
const monthly = extractQuotaWindow(responseData, mapping, "monthly");
|
|
1255
|
+
const todayStats = extractTokenStatsPeriod(responseData, mapping, "tokenStats.today");
|
|
1256
|
+
const totalStats = extractTokenStatsPeriod(responseData, mapping, "tokenStats.total");
|
|
1257
|
+
const rpm = extractNumber(responseData, mapping["tokenStats.rpm"]);
|
|
1258
|
+
const tpm = extractNumber(responseData, mapping["tokenStats.tpm"]);
|
|
1259
|
+
const tokenStats = todayStats || totalStats || rpm !== null || tpm !== null ? {
|
|
1260
|
+
today: todayStats,
|
|
1261
|
+
total: totalStats,
|
|
1262
|
+
rpm,
|
|
1263
|
+
tpm
|
|
1264
|
+
} : null;
|
|
1265
|
+
const rateLimit = (() => {
|
|
1266
|
+
const windowSeconds = extractNumber(responseData, mapping["rateLimit.windowSeconds"]);
|
|
1267
|
+
if (windowSeconds === null)
|
|
1268
|
+
return null;
|
|
1269
|
+
return {
|
|
1270
|
+
windowSeconds,
|
|
1271
|
+
requestsUsed: extractNumber(responseData, mapping["rateLimit.requestsUsed"]) ?? 0,
|
|
1272
|
+
requestsLimit: extractNumber(responseData, mapping["rateLimit.requestsLimit"]),
|
|
1273
|
+
costUsed: extractNumber(responseData, mapping["rateLimit.costUsed"]) ?? 0,
|
|
1274
|
+
costLimit: extractNumber(responseData, mapping["rateLimit.costLimit"]),
|
|
1275
|
+
remainingSeconds: extractNumber(responseData, mapping["rateLimit.remainingSeconds"]) ?? 0
|
|
1276
|
+
};
|
|
1277
|
+
})();
|
|
1278
|
+
const resetsAt = (() => {
|
|
1279
|
+
const times = [];
|
|
1280
|
+
if (daily?.resetsAt)
|
|
1281
|
+
times.push(daily.resetsAt);
|
|
1282
|
+
if (weekly?.resetsAt)
|
|
1283
|
+
times.push(weekly.resetsAt);
|
|
1284
|
+
if (monthly?.resetsAt)
|
|
1285
|
+
times.push(monthly.resetsAt);
|
|
1286
|
+
if (times.length === 0)
|
|
1287
|
+
return null;
|
|
1288
|
+
const sorted = [...times].sort();
|
|
1289
|
+
return sorted[0] ?? null;
|
|
1290
|
+
})();
|
|
1291
|
+
return {
|
|
1292
|
+
...base,
|
|
1293
|
+
resetSemantics: billingMode === "balance" ? "expiry" : "end-of-day",
|
|
1294
|
+
balance,
|
|
1295
|
+
daily,
|
|
1296
|
+
weekly,
|
|
1297
|
+
monthly,
|
|
1298
|
+
tokenStats,
|
|
1299
|
+
rateLimit,
|
|
1300
|
+
resetsAt
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/providers/custom.ts
|
|
1023
1305
|
function validateCustomProvider(providerConfig) {
|
|
1024
1306
|
if (!providerConfig.id)
|
|
1025
1307
|
return "Custom provider missing required field: id";
|
|
@@ -1048,7 +1330,7 @@ function validateCustomProvider(providerConfig) {
|
|
|
1048
1330
|
}
|
|
1049
1331
|
return null;
|
|
1050
1332
|
}
|
|
1051
|
-
async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs =
|
|
1333
|
+
async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
1052
1334
|
const validationError = validateCustomProvider(providerConfig);
|
|
1053
1335
|
if (validationError) {
|
|
1054
1336
|
throw new Error(`Invalid custom provider providerConfig: ${validationError}`);
|
|
@@ -1087,117 +1369,7 @@ async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs
|
|
|
1087
1369
|
body
|
|
1088
1370
|
}, timeoutMs, resolvedUA);
|
|
1089
1371
|
const responseData = JSON.parse(responseText);
|
|
1090
|
-
const
|
|
1091
|
-
const billingModeStr = extractString(responseData, mapping.billingMode, "subscription");
|
|
1092
|
-
const billingMode = billingModeStr === "balance" ? "balance" : "subscription";
|
|
1093
|
-
const planName = extractString(responseData, mapping.planName, providerConfig.displayName ?? providerConfig.id);
|
|
1094
|
-
const result = createEmptyNormalizedUsage(providerConfig.id, billingMode, planName);
|
|
1095
|
-
result.resetSemantics = billingMode === "balance" ? "expiry" : "end-of-day";
|
|
1096
|
-
if (mapping["balance.remaining"]) {
|
|
1097
|
-
const remaining = extractNumber(responseData, mapping["balance.remaining"]);
|
|
1098
|
-
if (remaining !== null) {
|
|
1099
|
-
result.balance = {
|
|
1100
|
-
remaining,
|
|
1101
|
-
initial: extractNumber(responseData, mapping["balance.initial"]),
|
|
1102
|
-
unit: extractString(responseData, mapping["balance.unit"], "USD")
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
const dailyUsed = extractNumber(responseData, mapping["daily.used"]);
|
|
1107
|
-
const dailyLimitRaw = extractNumber(responseData, mapping["daily.limit"]);
|
|
1108
|
-
const dailyLimit = dailyLimitRaw === 0 ? null : dailyLimitRaw;
|
|
1109
|
-
if (dailyUsed !== null) {
|
|
1110
|
-
result.daily = {
|
|
1111
|
-
used: dailyUsed,
|
|
1112
|
-
limit: dailyLimit,
|
|
1113
|
-
remaining: dailyLimit !== null ? Math.max(0, dailyLimit - dailyUsed) : null,
|
|
1114
|
-
resetsAt: extractString(responseData, mapping["daily.resetsAt"], "") || null
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
const weeklyUsed = extractNumber(responseData, mapping["weekly.used"]);
|
|
1118
|
-
const weeklyLimitRaw = extractNumber(responseData, mapping["weekly.limit"]);
|
|
1119
|
-
const weeklyLimit = weeklyLimitRaw === 0 ? null : weeklyLimitRaw;
|
|
1120
|
-
if (weeklyUsed !== null) {
|
|
1121
|
-
result.weekly = {
|
|
1122
|
-
used: weeklyUsed,
|
|
1123
|
-
limit: weeklyLimit,
|
|
1124
|
-
remaining: weeklyLimit !== null ? Math.max(0, weeklyLimit - weeklyUsed) : null,
|
|
1125
|
-
resetsAt: extractString(responseData, mapping["weekly.resetsAt"], "") || null
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
const monthlyUsed = extractNumber(responseData, mapping["monthly.used"]);
|
|
1129
|
-
const monthlyLimitRaw = extractNumber(responseData, mapping["monthly.limit"]);
|
|
1130
|
-
const monthlyLimit = monthlyLimitRaw === 0 ? null : monthlyLimitRaw;
|
|
1131
|
-
if (monthlyUsed !== null) {
|
|
1132
|
-
result.monthly = {
|
|
1133
|
-
used: monthlyUsed,
|
|
1134
|
-
limit: monthlyLimit,
|
|
1135
|
-
remaining: monthlyLimit !== null ? Math.max(0, monthlyLimit - monthlyUsed) : null,
|
|
1136
|
-
resetsAt: extractString(responseData, mapping["monthly.resetsAt"], "") || null
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
if (mapping["tokenStats.today.requests"]) {
|
|
1140
|
-
const todayRequests = extractNumber(responseData, mapping["tokenStats.today.requests"]);
|
|
1141
|
-
if (todayRequests !== null) {
|
|
1142
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1143
|
-
result.tokenStats.today = {
|
|
1144
|
-
requests: todayRequests,
|
|
1145
|
-
inputTokens: extractNumber(responseData, mapping["tokenStats.today.inputTokens"]) ?? 0,
|
|
1146
|
-
outputTokens: extractNumber(responseData, mapping["tokenStats.today.outputTokens"]) ?? 0,
|
|
1147
|
-
cacheCreationTokens: extractNumber(responseData, mapping["tokenStats.today.cacheCreationTokens"]) ?? 0,
|
|
1148
|
-
cacheReadTokens: extractNumber(responseData, mapping["tokenStats.today.cacheReadTokens"]) ?? 0,
|
|
1149
|
-
totalTokens: extractNumber(responseData, mapping["tokenStats.today.totalTokens"]) ?? 0,
|
|
1150
|
-
cost: extractNumber(responseData, mapping["tokenStats.today.cost"]) ?? 0
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
if (mapping["tokenStats.total.requests"]) {
|
|
1155
|
-
const totalRequests = extractNumber(responseData, mapping["tokenStats.total.requests"]);
|
|
1156
|
-
if (totalRequests !== null) {
|
|
1157
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1158
|
-
result.tokenStats.total = {
|
|
1159
|
-
requests: totalRequests,
|
|
1160
|
-
inputTokens: extractNumber(responseData, mapping["tokenStats.total.inputTokens"]) ?? 0,
|
|
1161
|
-
outputTokens: extractNumber(responseData, mapping["tokenStats.total.outputTokens"]) ?? 0,
|
|
1162
|
-
cacheCreationTokens: extractNumber(responseData, mapping["tokenStats.total.cacheCreationTokens"]) ?? 0,
|
|
1163
|
-
cacheReadTokens: extractNumber(responseData, mapping["tokenStats.total.cacheReadTokens"]) ?? 0,
|
|
1164
|
-
totalTokens: extractNumber(responseData, mapping["tokenStats.total.totalTokens"]) ?? 0,
|
|
1165
|
-
cost: extractNumber(responseData, mapping["tokenStats.total.cost"]) ?? 0
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
if (mapping["tokenStats.rpm"]) {
|
|
1170
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1171
|
-
result.tokenStats.rpm = extractNumber(responseData, mapping["tokenStats.rpm"]);
|
|
1172
|
-
}
|
|
1173
|
-
if (mapping["tokenStats.tpm"]) {
|
|
1174
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1175
|
-
result.tokenStats.tpm = extractNumber(responseData, mapping["tokenStats.tpm"]);
|
|
1176
|
-
}
|
|
1177
|
-
if (mapping["rateLimit.windowSeconds"]) {
|
|
1178
|
-
const windowSeconds = extractNumber(responseData, mapping["rateLimit.windowSeconds"]);
|
|
1179
|
-
if (windowSeconds !== null) {
|
|
1180
|
-
result.rateLimit = {
|
|
1181
|
-
windowSeconds,
|
|
1182
|
-
requestsUsed: extractNumber(responseData, mapping["rateLimit.requestsUsed"]) ?? 0,
|
|
1183
|
-
requestsLimit: extractNumber(responseData, mapping["rateLimit.requestsLimit"]),
|
|
1184
|
-
costUsed: extractNumber(responseData, mapping["rateLimit.costUsed"]) ?? 0,
|
|
1185
|
-
costLimit: extractNumber(responseData, mapping["rateLimit.costLimit"]),
|
|
1186
|
-
remainingSeconds: extractNumber(responseData, mapping["rateLimit.remainingSeconds"]) ?? 0
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
if (result.daily?.resetsAt || result.weekly?.resetsAt || result.monthly?.resetsAt) {
|
|
1191
|
-
const times = [];
|
|
1192
|
-
if (result.daily?.resetsAt)
|
|
1193
|
-
times.push(result.daily.resetsAt);
|
|
1194
|
-
if (result.weekly?.resetsAt)
|
|
1195
|
-
times.push(result.weekly.resetsAt);
|
|
1196
|
-
if (result.monthly?.resetsAt)
|
|
1197
|
-
times.push(result.monthly.resetsAt);
|
|
1198
|
-
times.sort();
|
|
1199
|
-
result.resetsAt = times[0] ?? null;
|
|
1200
|
-
}
|
|
1372
|
+
const result = mapResponseToUsage(responseData, providerConfig.responseMapping, providerConfig);
|
|
1201
1373
|
return result;
|
|
1202
1374
|
}
|
|
1203
1375
|
|
|
@@ -1327,13 +1499,25 @@ var ANSI_COLORS = {
|
|
|
1327
1499
|
};
|
|
1328
1500
|
var ANSI_RESET = "\x1B[0m";
|
|
1329
1501
|
var ANSI_DIM = "\x1B[2m";
|
|
1330
|
-
function ansiColor(text, color) {
|
|
1502
|
+
function ansiColor(text, color, capabilities) {
|
|
1331
1503
|
if (!color)
|
|
1332
1504
|
return text;
|
|
1333
1505
|
if (ANSI_COLORS[color.toLowerCase()]) {
|
|
1334
1506
|
return `${ANSI_COLORS[color.toLowerCase()]}${text}${ANSI_RESET}`;
|
|
1335
1507
|
}
|
|
1336
1508
|
if (color.startsWith("#")) {
|
|
1509
|
+
const colorMode = capabilities?.colorMode ?? "truecolor";
|
|
1510
|
+
if (colorMode === "16") {
|
|
1511
|
+
const named = hexToNearestNamedAnsi(color);
|
|
1512
|
+
return named ? `${named}${text}${ANSI_RESET}` : text;
|
|
1513
|
+
}
|
|
1514
|
+
if (colorMode === "256") {
|
|
1515
|
+
const index = hexTo256(color);
|
|
1516
|
+
if (index !== null) {
|
|
1517
|
+
return `\x1B[38;5;${index}m${text}${ANSI_RESET}`;
|
|
1518
|
+
}
|
|
1519
|
+
return text;
|
|
1520
|
+
}
|
|
1337
1521
|
const rgb = hexToRgb(color);
|
|
1338
1522
|
if (rgb) {
|
|
1339
1523
|
return `\x1B[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}${ANSI_RESET}`;
|
|
@@ -1341,6 +1525,43 @@ function ansiColor(text, color) {
|
|
|
1341
1525
|
}
|
|
1342
1526
|
return text;
|
|
1343
1527
|
}
|
|
1528
|
+
function hexTo256(hex) {
|
|
1529
|
+
const rgb = hexToRgb(hex);
|
|
1530
|
+
if (!rgb)
|
|
1531
|
+
return null;
|
|
1532
|
+
const r6 = Math.round(rgb.r / 255 * 5);
|
|
1533
|
+
const g6 = Math.round(rgb.g / 255 * 5);
|
|
1534
|
+
const b6 = Math.round(rgb.b / 255 * 5);
|
|
1535
|
+
return 16 + 36 * r6 + 6 * g6 + b6;
|
|
1536
|
+
}
|
|
1537
|
+
var ANSI_COLOR_RGB = [
|
|
1538
|
+
{ name: "black", r: 0, g: 0, b: 0 },
|
|
1539
|
+
{ name: "red", r: 170, g: 0, b: 0 },
|
|
1540
|
+
{ name: "green", r: 0, g: 170, b: 0 },
|
|
1541
|
+
{ name: "yellow", r: 170, g: 170, b: 0 },
|
|
1542
|
+
{ name: "blue", r: 0, g: 0, b: 170 },
|
|
1543
|
+
{ name: "magenta", r: 170, g: 0, b: 170 },
|
|
1544
|
+
{ name: "cyan", r: 0, g: 170, b: 170 },
|
|
1545
|
+
{ name: "white", r: 170, g: 170, b: 170 }
|
|
1546
|
+
];
|
|
1547
|
+
function hexToNearestNamedAnsi(hex) {
|
|
1548
|
+
const rgb = hexToRgb(hex);
|
|
1549
|
+
if (!rgb)
|
|
1550
|
+
return null;
|
|
1551
|
+
let minDist = Infinity;
|
|
1552
|
+
let nearest = "white";
|
|
1553
|
+
for (const entry of ANSI_COLOR_RGB) {
|
|
1554
|
+
const dr = rgb.r - entry.r;
|
|
1555
|
+
const dg = rgb.g - entry.g;
|
|
1556
|
+
const db = rgb.b - entry.b;
|
|
1557
|
+
const dist = dr * dr + dg * dg + db * db;
|
|
1558
|
+
if (dist < minDist) {
|
|
1559
|
+
minDist = dist;
|
|
1560
|
+
nearest = entry.name;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return ANSI_COLORS[nearest] ?? null;
|
|
1564
|
+
}
|
|
1344
1565
|
function hexToRgb(hex) {
|
|
1345
1566
|
const cleanHex = hex.replace(/^#/, "");
|
|
1346
1567
|
if (cleanHex.length === 3) {
|
|
@@ -1395,10 +1616,14 @@ function resolveColorAlias(alias, usagePercent) {
|
|
|
1395
1616
|
}
|
|
1396
1617
|
}
|
|
1397
1618
|
|
|
1619
|
+
// src/renderer/transition.ts
|
|
1620
|
+
function isTransitionState(errorState) {
|
|
1621
|
+
return errorState === "switching-provider" || errorState === "new-credentials" || errorState === "new-endpoint" || errorState === "auth-error-waiting";
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1398
1624
|
// src/renderer/error.ts
|
|
1399
1625
|
function renderError(errorState, mode, provider, message, cacheAge) {
|
|
1400
|
-
|
|
1401
|
-
if (isTransition) {
|
|
1626
|
+
if (isTransitionState(errorState)) {
|
|
1402
1627
|
return renderTransitionState(errorState);
|
|
1403
1628
|
}
|
|
1404
1629
|
if (mode === "without-cache") {
|
|
@@ -1432,11 +1657,13 @@ function renderStandaloneError(errorState, provider, message) {
|
|
|
1432
1657
|
case "auth-error":
|
|
1433
1658
|
return `${warningIcon} Auth error`;
|
|
1434
1659
|
case "rate-limited":
|
|
1435
|
-
return `${warningIcon}
|
|
1660
|
+
return `${warningIcon} Usage limit reached`;
|
|
1436
1661
|
case "provider-unknown":
|
|
1437
1662
|
return `${warningIcon} Unknown provider`;
|
|
1438
1663
|
case "missing-env":
|
|
1439
1664
|
return `${warningIcon} Set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`;
|
|
1665
|
+
case "timeout":
|
|
1666
|
+
return `${warningIcon} Fetching...`;
|
|
1440
1667
|
case "network-error":
|
|
1441
1668
|
case "server-error":
|
|
1442
1669
|
case "parse-error":
|
|
@@ -1460,7 +1687,9 @@ function renderErrorIndicator(errorState, cacheAge) {
|
|
|
1460
1687
|
case "parse-error":
|
|
1461
1688
|
return "[parse error]";
|
|
1462
1689
|
case "rate-limited":
|
|
1463
|
-
return "[
|
|
1690
|
+
return "[limit reached]";
|
|
1691
|
+
case "timeout":
|
|
1692
|
+
return "[timeout]";
|
|
1464
1693
|
default:
|
|
1465
1694
|
return "[error]";
|
|
1466
1695
|
}
|
|
@@ -1471,7 +1700,7 @@ function renderStalenessIndicator(label, cacheAge, showAge = true) {
|
|
|
1471
1700
|
}
|
|
1472
1701
|
const stalenessLevel = getStalenessLevel(cacheAge);
|
|
1473
1702
|
let text = label;
|
|
1474
|
-
if (showAge && cacheAge >=
|
|
1703
|
+
if (showAge && cacheAge >= STALENESS_THRESHOLD_MINUTES) {
|
|
1475
1704
|
text = `[stale ${cacheAge}m]`;
|
|
1476
1705
|
}
|
|
1477
1706
|
switch (stalenessLevel) {
|
|
@@ -1484,9 +1713,9 @@ function renderStalenessIndicator(label, cacheAge, showAge = true) {
|
|
|
1484
1713
|
}
|
|
1485
1714
|
}
|
|
1486
1715
|
function getStalenessLevel(ageMinutes) {
|
|
1487
|
-
if (ageMinutes <
|
|
1716
|
+
if (ageMinutes < STALENESS_THRESHOLD_MINUTES) {
|
|
1488
1717
|
return "fresh";
|
|
1489
|
-
} else if (ageMinutes <=
|
|
1718
|
+
} else if (ageMinutes <= VERY_STALE_THRESHOLD_MINUTES) {
|
|
1490
1719
|
return "stale";
|
|
1491
1720
|
} else {
|
|
1492
1721
|
return "very-stale";
|
|
@@ -1504,6 +1733,8 @@ function getDefaultMessage(errorState) {
|
|
|
1504
1733
|
return "authentication failed";
|
|
1505
1734
|
case "rate-limited":
|
|
1506
1735
|
return "rate limited";
|
|
1736
|
+
case "timeout":
|
|
1737
|
+
return "fetching...";
|
|
1507
1738
|
default:
|
|
1508
1739
|
return "error";
|
|
1509
1740
|
}
|
|
@@ -1669,56 +1900,117 @@ var PROGRESS_ICONS = [
|
|
|
1669
1900
|
"\uDB82\uDEA4",
|
|
1670
1901
|
"\uDB82\uDEA5"
|
|
1671
1902
|
];
|
|
1672
|
-
|
|
1903
|
+
var TEXT_PROGRESS_ICONS = [
|
|
1904
|
+
"○",
|
|
1905
|
+
"◔",
|
|
1906
|
+
"◑",
|
|
1907
|
+
"◕",
|
|
1908
|
+
"●"
|
|
1909
|
+
];
|
|
1910
|
+
function calcNerdIconIndex(percent) {
|
|
1911
|
+
return Math.min(8, Math.ceil(percent / 12.5));
|
|
1912
|
+
}
|
|
1913
|
+
function calcTextIconIndex(percent) {
|
|
1914
|
+
return Math.min(4, Math.ceil(percent / 25));
|
|
1915
|
+
}
|
|
1916
|
+
function getProgressIcon(percent, nerdFontAvailable = true) {
|
|
1917
|
+
if (!nerdFontAvailable) {
|
|
1918
|
+
if (percent === null) {
|
|
1919
|
+
return TEXT_PROGRESS_ICONS[0] ?? "○";
|
|
1920
|
+
}
|
|
1921
|
+
const clampedPercent2 = Math.max(0, Math.min(100, percent));
|
|
1922
|
+
const index2 = calcTextIconIndex(clampedPercent2);
|
|
1923
|
+
return TEXT_PROGRESS_ICONS[index2] ?? TEXT_PROGRESS_ICONS[0] ?? "○";
|
|
1924
|
+
}
|
|
1673
1925
|
if (percent === null) {
|
|
1674
1926
|
return PROGRESS_ICONS[0] ?? "";
|
|
1675
1927
|
}
|
|
1676
1928
|
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
1677
|
-
const index =
|
|
1929
|
+
const index = calcNerdIconIndex(clampedPercent);
|
|
1678
1930
|
return PROGRESS_ICONS[index] ?? PROGRESS_ICONS[0] ?? "";
|
|
1679
1931
|
}
|
|
1680
1932
|
|
|
1933
|
+
// src/renderer/format.ts
|
|
1934
|
+
function formatCurrency(n) {
|
|
1935
|
+
return `$${Math.floor(n)}`;
|
|
1936
|
+
}
|
|
1937
|
+
function formatCurrencyQuota(used, limit) {
|
|
1938
|
+
return `${formatCurrency(used)}/$${Math.floor(limit)}`;
|
|
1939
|
+
}
|
|
1940
|
+
function formatCompactNumber(n) {
|
|
1941
|
+
const absN = Math.abs(n);
|
|
1942
|
+
const sign = n < 0 ? "-" : "";
|
|
1943
|
+
if (absN < 1000) {
|
|
1944
|
+
return `${sign}${Math.round(absN)}`;
|
|
1945
|
+
}
|
|
1946
|
+
let threshold;
|
|
1947
|
+
let suffix;
|
|
1948
|
+
if (absN >= 1e9) {
|
|
1949
|
+
threshold = 1e9;
|
|
1950
|
+
suffix = "B";
|
|
1951
|
+
} else if (absN >= 1e6) {
|
|
1952
|
+
threshold = 1e6;
|
|
1953
|
+
suffix = "M";
|
|
1954
|
+
} else {
|
|
1955
|
+
threshold = 1000;
|
|
1956
|
+
suffix = "K";
|
|
1957
|
+
const kValue = absN / 1000;
|
|
1958
|
+
if (Math.round(kValue) >= 1000) {
|
|
1959
|
+
threshold = 1e6;
|
|
1960
|
+
suffix = "M";
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
const value = absN / threshold;
|
|
1964
|
+
const roundedToOneDec = Math.round(value * 10) / 10;
|
|
1965
|
+
if (roundedToOneDec < 10) {
|
|
1966
|
+
return `${sign}${roundedToOneDec.toFixed(1)}${suffix}`;
|
|
1967
|
+
} else {
|
|
1968
|
+
return `${sign}${Math.round(value)}${suffix}`;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1681
1972
|
// src/renderer/component.ts
|
|
1682
|
-
function renderComponent(componentId, data, componentConfig, globalConfig) {
|
|
1973
|
+
function renderComponent(componentId, data, componentConfig, globalConfig, renderContext) {
|
|
1683
1974
|
const effectiveLayout = componentConfig.layout ?? globalConfig.display.layout;
|
|
1684
|
-
const effectiveDisplayMode = componentConfig.displayMode ?? globalConfig.display.displayMode;
|
|
1975
|
+
const effectiveDisplayMode = resolveEffectiveDisplayMode(componentConfig.displayMode ?? globalConfig.display.displayMode, renderContext);
|
|
1976
|
+
const effectiveProgressStyle = resolveEffectiveProgressStyle(componentConfig.progressStyle ?? globalConfig.display.progressStyle, renderContext);
|
|
1685
1977
|
const effectiveBarSize = componentConfig.barSize ?? globalConfig.display.barSize;
|
|
1686
1978
|
const effectiveBarStyle = componentConfig.barStyle ?? globalConfig.display.barStyle;
|
|
1687
1979
|
const clockFormat = globalConfig.display.clockFormat;
|
|
1688
1980
|
switch (componentId) {
|
|
1689
1981
|
case "daily":
|
|
1690
|
-
return renderQuotaComponent("daily", data.daily, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat);
|
|
1982
|
+
return renderQuotaComponent("daily", data.daily, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
|
|
1691
1983
|
case "weekly":
|
|
1692
|
-
return renderQuotaComponent("weekly", data.weekly, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat);
|
|
1984
|
+
return renderQuotaComponent("weekly", data.weekly, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
|
|
1693
1985
|
case "monthly":
|
|
1694
|
-
return renderQuotaComponent("monthly", data.monthly, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat);
|
|
1986
|
+
return renderQuotaComponent("monthly", data.monthly, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
|
|
1695
1987
|
case "balance":
|
|
1696
|
-
return renderBalanceComponent(data.balance, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig);
|
|
1988
|
+
return renderBalanceComponent(data.balance, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, renderContext);
|
|
1697
1989
|
case "tokens":
|
|
1698
|
-
return renderTokensComponent(data.tokenStats, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig);
|
|
1990
|
+
return renderTokensComponent(data.tokenStats, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig, renderContext);
|
|
1699
1991
|
case "rateLimit":
|
|
1700
|
-
return renderRateLimitComponent(data.rateLimit, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig);
|
|
1992
|
+
return renderRateLimitComponent(data.rateLimit, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, renderContext);
|
|
1701
1993
|
case "plan":
|
|
1702
|
-
return renderPlanComponent(data.planName, effectiveLayout, componentConfig, globalConfig);
|
|
1994
|
+
return renderPlanComponent(data.planName, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig, renderContext);
|
|
1703
1995
|
default:
|
|
1704
1996
|
return null;
|
|
1705
1997
|
}
|
|
1706
1998
|
}
|
|
1707
|
-
function renderQuotaComponent(componentId, quota, layout, displayMode, barSize, barStyle, componentConfig, globalConfig, clockFormat) {
|
|
1999
|
+
function renderQuotaComponent(componentId, quota, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, clockFormat, renderContext) {
|
|
1708
2000
|
if (!quota)
|
|
1709
2001
|
return null;
|
|
1710
2002
|
const usagePercent = calculateUsagePercent(quota.used, quota.limit);
|
|
1711
|
-
const label = renderLabel(componentId,
|
|
2003
|
+
const label = renderLabel(componentId, displayMode, componentConfig, quota.qualifier);
|
|
1712
2004
|
const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
|
|
1713
2005
|
const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
|
|
1714
2006
|
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
1715
2007
|
const countdownColor = resolvePartColor("countdown", usagePercent, componentConfig, globalConfig);
|
|
1716
|
-
const
|
|
1717
|
-
const value = ansiColor(`${Math.round(usagePercent)}%`, valueColor);
|
|
1718
|
-
const countdown =
|
|
1719
|
-
return assembleComponent(layout, label, labelColor,
|
|
2008
|
+
const progress = renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, null);
|
|
2009
|
+
const value = ansiColor(`${Math.round(usagePercent)}%`, valueColor, renderContext);
|
|
2010
|
+
const countdown = renderSecondaryDisplay(quota.resetsAt, quota, componentConfig.countdown, countdownColor, clockFormat, renderContext);
|
|
2011
|
+
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
1720
2012
|
}
|
|
1721
|
-
function renderBalanceComponent(balance, layout, displayMode, barSize, barStyle, componentConfig, globalConfig) {
|
|
2013
|
+
function renderBalanceComponent(balance, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, renderContext) {
|
|
1722
2014
|
if (!balance)
|
|
1723
2015
|
return null;
|
|
1724
2016
|
const isUnlimited = balance.remaining === -1;
|
|
@@ -1727,120 +2019,171 @@ function renderBalanceComponent(balance, layout, displayMode, barSize, barStyle,
|
|
|
1727
2019
|
usagePercent = (balance.initial - balance.remaining) / balance.initial * 100;
|
|
1728
2020
|
}
|
|
1729
2021
|
const effectivePercent = isUnlimited ? 0 : usagePercent;
|
|
1730
|
-
const label = renderLabel("balance",
|
|
2022
|
+
const label = renderLabel("balance", displayMode, componentConfig);
|
|
1731
2023
|
const barColor = resolvePartColor("bar", effectivePercent, componentConfig, globalConfig);
|
|
1732
2024
|
const valueColor = resolvePartColor("value", effectivePercent, componentConfig, globalConfig);
|
|
1733
2025
|
const labelColor = resolvePartColor("label", effectivePercent, componentConfig, globalConfig);
|
|
1734
|
-
const
|
|
2026
|
+
const progress = isUnlimited ? "" : renderProgress(progressStyle, effectivePercent ?? 0, barSize, barStyle, barColor, null);
|
|
1735
2027
|
const valueText = isUnlimited ? "∞" : `$${balance.remaining.toFixed(2)}`;
|
|
1736
|
-
const value = ansiColor(valueText, valueColor);
|
|
2028
|
+
const value = ansiColor(valueText, valueColor, renderContext);
|
|
1737
2029
|
const countdown = "";
|
|
1738
|
-
return assembleComponent(layout, label, labelColor,
|
|
2030
|
+
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
1739
2031
|
}
|
|
1740
|
-
function renderTokensComponent(tokenStats, layout, displayMode, componentConfig, globalConfig) {
|
|
2032
|
+
function renderTokensComponent(tokenStats, layout, displayMode, componentConfig, globalConfig, renderContext) {
|
|
1741
2033
|
if (!tokenStats)
|
|
1742
2034
|
return null;
|
|
1743
2035
|
const stats = tokenStats.total ?? tokenStats.today;
|
|
1744
2036
|
if (!stats)
|
|
1745
2037
|
return null;
|
|
1746
|
-
const label = renderLabel("tokens",
|
|
2038
|
+
const label = renderLabel("tokens", displayMode, componentConfig);
|
|
1747
2039
|
const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
|
|
1748
2040
|
const valueColor = resolvePartColor("value", null, componentConfig, globalConfig);
|
|
1749
2041
|
const tokenCount = stats.totalTokens ?? stats.inputTokens + stats.outputTokens;
|
|
1750
|
-
const valueText =
|
|
1751
|
-
const value = ansiColor(valueText, valueColor);
|
|
1752
|
-
const
|
|
2042
|
+
const valueText = formatCompactNumber(tokenCount);
|
|
2043
|
+
const value = ansiColor(valueText, valueColor, renderContext);
|
|
2044
|
+
const progress = "";
|
|
1753
2045
|
const countdown = "";
|
|
1754
|
-
return assembleComponent(layout, label, labelColor,
|
|
2046
|
+
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
1755
2047
|
}
|
|
1756
|
-
function renderRateLimitComponent(rateLimit, layout, displayMode, barSize, barStyle, componentConfig, globalConfig) {
|
|
2048
|
+
function renderRateLimitComponent(rateLimit, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, renderContext) {
|
|
1757
2049
|
if (!rateLimit)
|
|
1758
2050
|
return null;
|
|
1759
2051
|
let usagePercent = null;
|
|
1760
2052
|
if (rateLimit.requestsLimit !== null && rateLimit.requestsLimit > 0) {
|
|
1761
2053
|
usagePercent = rateLimit.requestsUsed / rateLimit.requestsLimit * 100;
|
|
1762
2054
|
}
|
|
1763
|
-
const label = renderLabel("rateLimit",
|
|
2055
|
+
const label = renderLabel("rateLimit", displayMode, componentConfig);
|
|
1764
2056
|
const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
|
|
1765
2057
|
const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
|
|
1766
2058
|
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
1767
|
-
const
|
|
2059
|
+
const progress = usagePercent !== null ? renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, null) : "";
|
|
1768
2060
|
const valueText = rateLimit.requestsLimit !== null ? `${rateLimit.requestsUsed}/${rateLimit.requestsLimit}` : `${rateLimit.requestsUsed}`;
|
|
1769
|
-
const value = ansiColor(valueText, valueColor);
|
|
2061
|
+
const value = ansiColor(valueText, valueColor, renderContext);
|
|
1770
2062
|
const countdown = "";
|
|
1771
|
-
return assembleComponent(layout, label, labelColor,
|
|
2063
|
+
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
1772
2064
|
}
|
|
1773
|
-
function renderPlanComponent(planName, layout, componentConfig, globalConfig) {
|
|
1774
|
-
if (
|
|
2065
|
+
function renderPlanComponent(planName, layout, displayMode, componentConfig, globalConfig, renderContext) {
|
|
2066
|
+
if (displayMode === "hidden")
|
|
1775
2067
|
return null;
|
|
1776
2068
|
const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
|
|
1777
|
-
const value = ansiColor(planName, labelColor);
|
|
2069
|
+
const value = ansiColor(planName, labelColor, renderContext);
|
|
1778
2070
|
if (typeof componentConfig.label === "object" && componentConfig.label.text) {
|
|
1779
|
-
const labelText = ansiColor(componentConfig.label.text, labelColor);
|
|
2071
|
+
const labelText = ansiColor(componentConfig.label.text, labelColor, renderContext);
|
|
1780
2072
|
return `${labelText} ${value}`;
|
|
1781
2073
|
}
|
|
1782
2074
|
return value;
|
|
1783
2075
|
}
|
|
1784
|
-
function renderLabel(componentId,
|
|
1785
|
-
if (
|
|
2076
|
+
function renderLabel(componentId, displayMode, componentConfig, qualifier) {
|
|
2077
|
+
if (displayMode === "hidden")
|
|
1786
2078
|
return "";
|
|
1787
2079
|
if (componentConfig.label === false)
|
|
1788
2080
|
return "";
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2081
|
+
const label = componentConfig.label;
|
|
2082
|
+
let baseLabel;
|
|
2083
|
+
switch (displayMode) {
|
|
2084
|
+
case "emoji": {
|
|
2085
|
+
if (typeof label === "object" && label.emoji) {
|
|
2086
|
+
baseLabel = label.emoji;
|
|
2087
|
+
} else {
|
|
2088
|
+
baseLabel = COMPONENT_EMOJI_LABELS[componentId] ?? COMPONENT_FULL_LABELS[componentId] ?? "";
|
|
2089
|
+
}
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
case "nerd": {
|
|
2093
|
+
if (typeof label === "object" && label.nerd) {
|
|
2094
|
+
baseLabel = label.nerd;
|
|
2095
|
+
} else {
|
|
2096
|
+
baseLabel = COMPONENT_NERD_LABELS[componentId] ?? COMPONENT_FULL_LABELS[componentId] ?? "";
|
|
2097
|
+
}
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2100
|
+
case "compact": {
|
|
2101
|
+
if (typeof label === "string") {
|
|
2102
|
+
baseLabel = label.charAt(0);
|
|
2103
|
+
} else if (typeof label === "object" && label.text) {
|
|
2104
|
+
baseLabel = label.text.charAt(0);
|
|
2105
|
+
} else {
|
|
2106
|
+
baseLabel = COMPONENT_SHORT_LABELS[componentId] ?? "";
|
|
2107
|
+
}
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
case "text":
|
|
2111
|
+
default: {
|
|
2112
|
+
if (typeof label === "string") {
|
|
2113
|
+
baseLabel = label;
|
|
2114
|
+
} else if (typeof label === "object" && label.text) {
|
|
2115
|
+
baseLabel = label.text;
|
|
2116
|
+
} else {
|
|
2117
|
+
baseLabel = COMPONENT_FULL_LABELS[componentId] ?? "";
|
|
2118
|
+
}
|
|
2119
|
+
break;
|
|
2120
|
+
}
|
|
1797
2121
|
}
|
|
1798
|
-
if (
|
|
1799
|
-
|
|
2122
|
+
if (qualifier) {
|
|
2123
|
+
if (displayMode === "compact") {
|
|
2124
|
+
return `${baseLabel}(${qualifier.charAt(0)})`;
|
|
2125
|
+
}
|
|
2126
|
+
return `${baseLabel}(${qualifier})`;
|
|
1800
2127
|
}
|
|
1801
|
-
return
|
|
2128
|
+
return baseLabel;
|
|
1802
2129
|
}
|
|
1803
|
-
function
|
|
1804
|
-
switch (
|
|
2130
|
+
function renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, emptyColor) {
|
|
2131
|
+
switch (progressStyle) {
|
|
1805
2132
|
case "bar":
|
|
1806
2133
|
return renderBar(usagePercent, barSize, barStyle, barColor, emptyColor);
|
|
1807
|
-
case "
|
|
2134
|
+
case "icon":
|
|
2135
|
+
return getProgressIcon(usagePercent, true);
|
|
2136
|
+
case "hidden":
|
|
1808
2137
|
return "";
|
|
1809
|
-
case "icon-pct":
|
|
1810
|
-
return getProgressIcon(usagePercent);
|
|
1811
2138
|
default:
|
|
1812
2139
|
return "";
|
|
1813
2140
|
}
|
|
1814
2141
|
}
|
|
1815
|
-
function
|
|
1816
|
-
if (
|
|
2142
|
+
function resolveEffectiveProgressStyle(requested, renderContext) {
|
|
2143
|
+
if (requested === "icon" && renderContext && !renderContext.nerdFontAvailable) {
|
|
2144
|
+
return "bar";
|
|
2145
|
+
}
|
|
2146
|
+
return requested;
|
|
2147
|
+
}
|
|
2148
|
+
function resolveEffectiveDisplayMode(requested, renderContext) {
|
|
2149
|
+
if (requested === "nerd" && renderContext && !renderContext.nerdFontAvailable) {
|
|
2150
|
+
return "text";
|
|
2151
|
+
}
|
|
2152
|
+
return requested;
|
|
2153
|
+
}
|
|
2154
|
+
function renderSecondaryDisplay(resetsAt, quota, countdownConfig, countdownColor, clockFormat, renderContext) {
|
|
2155
|
+
if (countdownConfig === false)
|
|
1817
2156
|
return "";
|
|
1818
2157
|
const config = typeof countdownConfig === "object" ? countdownConfig : {};
|
|
1819
|
-
const
|
|
1820
|
-
|
|
2158
|
+
const divider = config.divider ?? " · ";
|
|
2159
|
+
const prefix = config.prefix ?? "";
|
|
2160
|
+
if (quota.limit !== null && quota.limit > 0) {
|
|
2161
|
+
const costDisplay = formatCurrencyQuota(quota.used, quota.limit);
|
|
2162
|
+
const display = `${prefix}${divider}${costDisplay}`;
|
|
2163
|
+
return countdownColor ? ansiColor(display, countdownColor, renderContext) : display;
|
|
2164
|
+
}
|
|
2165
|
+
if (resetsAt !== null) {
|
|
2166
|
+
const countdown = renderCountdown(resetsAt, config, clockFormat);
|
|
2167
|
+
return countdownColor ? ansiColor(countdown, countdownColor, renderContext) : countdown;
|
|
2168
|
+
}
|
|
2169
|
+
return "";
|
|
1821
2170
|
}
|
|
1822
|
-
function assembleComponent(layout, label, labelColor,
|
|
1823
|
-
const coloredLabel = label && labelColor ? ansiColor(label, labelColor) : label;
|
|
2171
|
+
function assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext) {
|
|
2172
|
+
const coloredLabel = label && labelColor ? ansiColor(label, labelColor, renderContext) : label;
|
|
1824
2173
|
const parts = [];
|
|
1825
|
-
if (layout === "
|
|
1826
|
-
if (display)
|
|
1827
|
-
parts.push(display);
|
|
1828
|
-
parts.push(value);
|
|
1829
|
-
if (countdown)
|
|
1830
|
-
parts.push(countdown);
|
|
1831
|
-
} else if (layout === "percent-first") {
|
|
2174
|
+
if (layout === "percent-first") {
|
|
1832
2175
|
if (coloredLabel)
|
|
1833
2176
|
parts.push(coloredLabel);
|
|
1834
2177
|
parts.push(value);
|
|
1835
|
-
if (
|
|
1836
|
-
parts.push(
|
|
2178
|
+
if (progress)
|
|
2179
|
+
parts.push(progress);
|
|
1837
2180
|
if (countdown)
|
|
1838
2181
|
parts.push(countdown);
|
|
1839
2182
|
} else {
|
|
1840
2183
|
if (coloredLabel)
|
|
1841
2184
|
parts.push(coloredLabel);
|
|
1842
|
-
if (
|
|
1843
|
-
parts.push(
|
|
2185
|
+
if (progress)
|
|
2186
|
+
parts.push(progress);
|
|
1844
2187
|
parts.push(value);
|
|
1845
2188
|
if (countdown)
|
|
1846
2189
|
parts.push(countdown);
|
|
@@ -1848,8 +2191,10 @@ function assembleComponent(layout, label, labelColor, display, value, countdown)
|
|
|
1848
2191
|
return parts.filter((p) => p).join(" ").replace(/ (\x1b\[[0-9;]*m)? ([·•])/g, "$1 $2");
|
|
1849
2192
|
}
|
|
1850
2193
|
function calculateUsagePercent(used, limit) {
|
|
1851
|
-
if (limit === null
|
|
2194
|
+
if (limit === null)
|
|
1852
2195
|
return 0;
|
|
2196
|
+
if (limit === 0)
|
|
2197
|
+
return 100;
|
|
1853
2198
|
return used / limit * 100;
|
|
1854
2199
|
}
|
|
1855
2200
|
function resolvePartColor(part, usagePercent, componentConfig, globalConfig) {
|
|
@@ -1860,16 +2205,6 @@ function resolvePartColor(part, usagePercent, componentConfig, globalConfig) {
|
|
|
1860
2205
|
const color = componentConfig.color ?? "auto";
|
|
1861
2206
|
return resolveColor(color, usagePercent, globalConfig);
|
|
1862
2207
|
}
|
|
1863
|
-
function formatLargeNumber(n) {
|
|
1864
|
-
if (n >= 1e9) {
|
|
1865
|
-
return `${(n / 1e9).toFixed(1)}B`;
|
|
1866
|
-
} else if (n >= 1e6) {
|
|
1867
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
1868
|
-
} else if (n >= 1000) {
|
|
1869
|
-
return `${(n / 1000).toFixed(1)}K`;
|
|
1870
|
-
}
|
|
1871
|
-
return n.toString();
|
|
1872
|
-
}
|
|
1873
2208
|
|
|
1874
2209
|
// src/renderer/truncate.ts
|
|
1875
2210
|
function getTerminalWidth() {
|
|
@@ -1935,17 +2270,75 @@ var COMPONENT_DROP_PRIORITY = [
|
|
|
1935
2270
|
"balance"
|
|
1936
2271
|
];
|
|
1937
2272
|
|
|
2273
|
+
// src/renderer/divider.ts
|
|
2274
|
+
function renderDivider(divider) {
|
|
2275
|
+
const text = divider.text ?? "|";
|
|
2276
|
+
const padding = divider.padding ?? 1;
|
|
2277
|
+
const pad = " ".repeat(padding);
|
|
2278
|
+
const padded = `${pad}${text}${pad}`;
|
|
2279
|
+
return divider.color ? ansiColor(padded, divider.color) : padded;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// src/services/capabilities.ts
|
|
2283
|
+
var TRUECOLOR_TERMINALS = ["iTerm.app", "WezTerm", "Alacritty", "kitty", "Hyper", "vscode"];
|
|
2284
|
+
var NERD_TERMINALS = ["iTerm.app", "WezTerm", "Alacritty", "kitty", "Hyper"];
|
|
2285
|
+
function detectColorMode() {
|
|
2286
|
+
if (process.env["NO_COLOR"] !== undefined) {
|
|
2287
|
+
return "16";
|
|
2288
|
+
}
|
|
2289
|
+
const colorterm = process.env["COLORTERM"] ?? "";
|
|
2290
|
+
if (colorterm === "truecolor" || colorterm === "24bit") {
|
|
2291
|
+
return "truecolor";
|
|
2292
|
+
}
|
|
2293
|
+
const termProgram = process.env["TERM_PROGRAM"] ?? "";
|
|
2294
|
+
if (TRUECOLOR_TERMINALS.some((t) => termProgram.includes(t))) {
|
|
2295
|
+
return "truecolor";
|
|
2296
|
+
}
|
|
2297
|
+
const term = process.env["TERM"] ?? "";
|
|
2298
|
+
if (term.includes("256color")) {
|
|
2299
|
+
return "256";
|
|
2300
|
+
}
|
|
2301
|
+
return "truecolor";
|
|
2302
|
+
}
|
|
2303
|
+
function resolveColorMode(configured) {
|
|
2304
|
+
if (!configured || configured === "auto") {
|
|
2305
|
+
return detectColorMode();
|
|
2306
|
+
}
|
|
2307
|
+
return configured;
|
|
2308
|
+
}
|
|
2309
|
+
function detectNerdFont() {
|
|
2310
|
+
const override = process.env["CC_STATUSLINE_NERD_FONT"];
|
|
2311
|
+
if (override === "1" || override === "true")
|
|
2312
|
+
return true;
|
|
2313
|
+
if (override === "0" || override === "false")
|
|
2314
|
+
return false;
|
|
2315
|
+
const termProgram = process.env["TERM_PROGRAM"] ?? "";
|
|
2316
|
+
if (NERD_TERMINALS.some((t) => termProgram.includes(t)))
|
|
2317
|
+
return true;
|
|
2318
|
+
if (termProgram === "vscode")
|
|
2319
|
+
return true;
|
|
2320
|
+
return true;
|
|
2321
|
+
}
|
|
2322
|
+
function resolveNerdFont(configured) {
|
|
2323
|
+
if (configured === true)
|
|
2324
|
+
return true;
|
|
2325
|
+
if (configured === false)
|
|
2326
|
+
return false;
|
|
2327
|
+
return detectNerdFont();
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/renderer/context.ts
|
|
2331
|
+
function createRenderContext(config, isPiped) {
|
|
2332
|
+
return {
|
|
2333
|
+
colorMode: resolveColorMode(config.display.colorMode),
|
|
2334
|
+
nerdFontAvailable: resolveNerdFont(config.display.nerdFont),
|
|
2335
|
+
isPiped
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
|
|
1938
2339
|
// src/renderer/index.ts
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
"weekly",
|
|
1942
|
-
"monthly",
|
|
1943
|
-
"balance",
|
|
1944
|
-
"tokens",
|
|
1945
|
-
"rateLimit",
|
|
1946
|
-
"plan"
|
|
1947
|
-
];
|
|
1948
|
-
function renderStatusline(data, config, errorState, cacheAge) {
|
|
2340
|
+
function renderStatusline(data, config, errorState, cacheAge, isPiped = false) {
|
|
2341
|
+
const renderContext = createRenderContext(config, isPiped);
|
|
1949
2342
|
const componentOrder = getComponentOrder(config);
|
|
1950
2343
|
const componentMap = new Map;
|
|
1951
2344
|
for (const componentId of componentOrder) {
|
|
@@ -1953,26 +2346,21 @@ function renderStatusline(data, config, errorState, cacheAge) {
|
|
|
1953
2346
|
if (componentConfig === false) {
|
|
1954
2347
|
continue;
|
|
1955
2348
|
}
|
|
1956
|
-
const rendered = renderComponent(componentId, data, componentConfig === true || componentConfig === undefined ? {} : componentConfig, config);
|
|
2349
|
+
const rendered = renderComponent(componentId, data, componentConfig === true || componentConfig === undefined ? {} : componentConfig, config, renderContext);
|
|
1957
2350
|
if (rendered !== null) {
|
|
1958
2351
|
componentMap.set(componentId, rendered);
|
|
1959
2352
|
}
|
|
1960
2353
|
}
|
|
1961
|
-
const
|
|
1962
|
-
const maxWidth = computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
|
|
1963
|
-
const separator = config.display.separator ?? " | ";
|
|
2354
|
+
const separator = computeSeparator(config);
|
|
1964
2355
|
const activeComponents = new Set(componentMap.keys());
|
|
1965
2356
|
let currentWidth = calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge);
|
|
1966
2357
|
for (const dropCandidate of COMPONENT_DROP_PRIORITY) {
|
|
1967
|
-
if (dropCandidate === "countdown")
|
|
2358
|
+
if (dropCandidate === "countdown")
|
|
1968
2359
|
continue;
|
|
1969
|
-
|
|
1970
|
-
if (currentWidth <= maxWidth) {
|
|
2360
|
+
if (currentWidth <= maxWidth(config))
|
|
1971
2361
|
break;
|
|
1972
|
-
|
|
1973
|
-
if (activeComponents.size <= 1) {
|
|
2362
|
+
if (activeComponents.size <= 1)
|
|
1974
2363
|
break;
|
|
1975
|
-
}
|
|
1976
2364
|
if (activeComponents.has(dropCandidate)) {
|
|
1977
2365
|
activeComponents.delete(dropCandidate);
|
|
1978
2366
|
currentWidth = calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge);
|
|
@@ -1982,54 +2370,56 @@ function renderStatusline(data, config, errorState, cacheAge) {
|
|
|
1982
2370
|
for (const componentId of componentOrder) {
|
|
1983
2371
|
if (activeComponents.has(componentId)) {
|
|
1984
2372
|
const rendered = componentMap.get(componentId);
|
|
1985
|
-
if (rendered)
|
|
2373
|
+
if (rendered)
|
|
1986
2374
|
renderedComponents.push(rendered);
|
|
1987
|
-
}
|
|
1988
2375
|
}
|
|
1989
2376
|
}
|
|
1990
2377
|
let statusline = renderedComponents.join(separator);
|
|
1991
2378
|
if (errorState) {
|
|
1992
|
-
|
|
1993
|
-
if (isTransition) {
|
|
2379
|
+
if (isTransitionState(errorState)) {
|
|
1994
2380
|
statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
|
|
1995
2381
|
} else {
|
|
1996
2382
|
const hasCache = renderedComponents.length > 0;
|
|
1997
2383
|
const errorMode = hasCache ? "with-cache" : "without-cache";
|
|
1998
2384
|
const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
|
|
1999
|
-
|
|
2000
|
-
statusline = `${statusline} ${errorIndicator}`;
|
|
2001
|
-
} else {
|
|
2002
|
-
statusline = errorIndicator;
|
|
2003
|
-
}
|
|
2385
|
+
statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
|
|
2004
2386
|
}
|
|
2005
2387
|
}
|
|
2006
|
-
|
|
2388
|
+
const termWidth = getTerminalWidth();
|
|
2389
|
+
const maxW = computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
|
|
2390
|
+
statusline = ansiAwareTruncate(statusline, maxW);
|
|
2007
2391
|
return statusline;
|
|
2008
2392
|
}
|
|
2009
|
-
function
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2393
|
+
function computeSeparator(config) {
|
|
2394
|
+
const dividerConfig = config.components.divider;
|
|
2395
|
+
if (dividerConfig === false)
|
|
2396
|
+
return "";
|
|
2397
|
+
if (typeof dividerConfig === "object")
|
|
2398
|
+
return renderDivider(dividerConfig);
|
|
2399
|
+
return config.display.separator ?? " | ";
|
|
2400
|
+
}
|
|
2401
|
+
function maxWidth(config) {
|
|
2402
|
+
const termWidth = getTerminalWidth();
|
|
2403
|
+
return computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
|
|
2404
|
+
}
|
|
2405
|
+
function calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge) {
|
|
2406
|
+
const components = [];
|
|
2407
|
+
for (const id of componentOrder) {
|
|
2012
2408
|
if (activeComponents.has(id)) {
|
|
2013
2409
|
const rendered = componentMap.get(id);
|
|
2014
|
-
if (rendered)
|
|
2410
|
+
if (rendered)
|
|
2015
2411
|
components.push(rendered);
|
|
2016
|
-
}
|
|
2017
2412
|
}
|
|
2018
2413
|
}
|
|
2019
2414
|
let statusline = components.join(separator);
|
|
2020
2415
|
if (errorState) {
|
|
2021
|
-
|
|
2022
|
-
if (isTransition) {
|
|
2416
|
+
if (isTransitionState(errorState)) {
|
|
2023
2417
|
statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
|
|
2024
2418
|
} else {
|
|
2025
2419
|
const hasCache = components.length > 0;
|
|
2026
2420
|
const errorMode = hasCache ? "with-cache" : "without-cache";
|
|
2027
2421
|
const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
|
|
2028
|
-
|
|
2029
|
-
statusline = `${statusline} ${errorIndicator}`;
|
|
2030
|
-
} else {
|
|
2031
|
-
statusline = errorIndicator;
|
|
2032
|
-
}
|
|
2422
|
+
statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
|
|
2033
2423
|
}
|
|
2034
2424
|
}
|
|
2035
2425
|
return visibleLength(statusline);
|
|
@@ -2044,7 +2434,7 @@ function getComponentOrder(config) {
|
|
|
2044
2434
|
}
|
|
2045
2435
|
}
|
|
2046
2436
|
const order = [...explicitOrder];
|
|
2047
|
-
for (const componentId of
|
|
2437
|
+
for (const componentId of DEFAULT_COMPONENT_ORDER) {
|
|
2048
2438
|
if (!explicitSet.has(componentId)) {
|
|
2049
2439
|
order.push(componentId);
|
|
2050
2440
|
}
|
|
@@ -2052,7 +2442,7 @@ function getComponentOrder(config) {
|
|
|
2052
2442
|
return order;
|
|
2053
2443
|
}
|
|
2054
2444
|
function isComponentId(key) {
|
|
2055
|
-
return
|
|
2445
|
+
return DEFAULT_COMPONENT_ORDER.includes(key);
|
|
2056
2446
|
}
|
|
2057
2447
|
|
|
2058
2448
|
// src/core/execute-cycle.ts
|
|
@@ -2086,23 +2476,16 @@ async function executeCycle(ctx) {
|
|
|
2086
2476
|
cacheUpdate: updatedEntry
|
|
2087
2477
|
};
|
|
2088
2478
|
}
|
|
2089
|
-
const deadline = startTime + timeoutBudgetMs -
|
|
2479
|
+
const deadline = startTime + timeoutBudgetMs - EXIT_BUFFER_MS;
|
|
2090
2480
|
const remainingBudget = deadline - Date.now();
|
|
2091
2481
|
if (remainingBudget <= 50) {
|
|
2092
|
-
logger.debug("Path
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
} else {
|
|
2100
|
-
return {
|
|
2101
|
-
output: "[loading...]",
|
|
2102
|
-
exitCode: 0,
|
|
2103
|
-
cacheUpdate: null
|
|
2104
|
-
};
|
|
2105
|
-
}
|
|
2482
|
+
logger.debug("Path D1: Timeout fallback", { remainingBudget });
|
|
2483
|
+
const errorOutput = renderError("timeout", "without-cache", providerId);
|
|
2484
|
+
return {
|
|
2485
|
+
output: errorOutput,
|
|
2486
|
+
exitCode: 0,
|
|
2487
|
+
cacheUpdate: null
|
|
2488
|
+
};
|
|
2106
2489
|
}
|
|
2107
2490
|
try {
|
|
2108
2491
|
const baseUrl = env.baseUrl;
|
|
@@ -2140,225 +2523,221 @@ async function executeCycle(ctx) {
|
|
|
2140
2523
|
};
|
|
2141
2524
|
} catch (error) {
|
|
2142
2525
|
logger.error("Path D: Fetch error", { error: String(error), hasCachedEntry: !!cachedEntry });
|
|
2526
|
+
let errorState = "network-error";
|
|
2527
|
+
if (error && typeof error === "object" && "statusCode" in error) {
|
|
2528
|
+
const statusCode = error.statusCode;
|
|
2529
|
+
if (statusCode === 429) {
|
|
2530
|
+
errorState = "rate-limited";
|
|
2531
|
+
} else if (statusCode && statusCode >= 500) {
|
|
2532
|
+
errorState = "server-error";
|
|
2533
|
+
} else if (statusCode === 401 || statusCode === 403) {
|
|
2534
|
+
errorState = "auth-error";
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2143
2537
|
if (cachedEntry) {
|
|
2144
|
-
|
|
2145
|
-
const statusline = renderStatusline(cachedEntry.data, config, "network-error", ageMinutes);
|
|
2146
|
-
logger.debug("Using stale cache with error indicator", { ageMinutes });
|
|
2147
|
-
return {
|
|
2148
|
-
output: statusline,
|
|
2149
|
-
exitCode: 0,
|
|
2150
|
-
cacheUpdate: null
|
|
2151
|
-
};
|
|
2538
|
+
logger.debug("Discarding stale cache, showing error", { errorState });
|
|
2152
2539
|
} else {
|
|
2153
2540
|
logger.warn("No cache available for error fallback");
|
|
2154
|
-
const errorOutput = renderError("network-error", "without-cache", providerId);
|
|
2155
|
-
return {
|
|
2156
|
-
output: errorOutput,
|
|
2157
|
-
exitCode: 0,
|
|
2158
|
-
cacheUpdate: null
|
|
2159
|
-
};
|
|
2160
2541
|
}
|
|
2542
|
+
const errorOutput = renderError(errorState, "without-cache", providerId);
|
|
2543
|
+
return {
|
|
2544
|
+
output: errorOutput,
|
|
2545
|
+
exitCode: 0,
|
|
2546
|
+
cacheUpdate: null
|
|
2547
|
+
};
|
|
2161
2548
|
}
|
|
2162
2549
|
}
|
|
2163
|
-
// src/services/
|
|
2164
|
-
import {
|
|
2165
|
-
import {
|
|
2166
|
-
|
|
2167
|
-
function loadClaudeSettings() {
|
|
2168
|
-
const path = getSettingsJsonPath();
|
|
2169
|
-
if (!existsSync5(path)) {
|
|
2170
|
-
return {};
|
|
2171
|
-
}
|
|
2172
|
-
try {
|
|
2173
|
-
const content = readFileSync4(path, "utf-8");
|
|
2174
|
-
return JSON.parse(content);
|
|
2175
|
-
} catch (error) {
|
|
2176
|
-
console.warn(`Failed to read settings from ${path}: ${error}`);
|
|
2177
|
-
return {};
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
function saveClaudeSettings(settings) {
|
|
2181
|
-
const path = getSettingsJsonPath();
|
|
2182
|
-
const tmpPath = `${path}.tmp`;
|
|
2550
|
+
// src/services/cache-gc.ts
|
|
2551
|
+
import { readdirSync, statSync, unlinkSync as unlinkSync3, existsSync as existsSync7 } from "fs";
|
|
2552
|
+
import { join as join6 } from "path";
|
|
2553
|
+
function runCacheGC(cacheDir) {
|
|
2183
2554
|
try {
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2555
|
+
if (!existsSync7(cacheDir)) {
|
|
2556
|
+
logger.debug("GC: Cache directory does not exist, skipping", { cacheDir });
|
|
2557
|
+
return;
|
|
2187
2558
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2559
|
+
logger.debug("GC: Starting garbage collection", { cacheDir });
|
|
2560
|
+
const files = readdirSync(cacheDir);
|
|
2561
|
+
const cacheFiles = [];
|
|
2562
|
+
const providerDetectFiles = [];
|
|
2563
|
+
const tmpFiles = [];
|
|
2564
|
+
for (const file of files) {
|
|
2565
|
+
try {
|
|
2566
|
+
const filePath = join6(cacheDir, file);
|
|
2567
|
+
const stats = statSync(filePath);
|
|
2568
|
+
const mtime = stats.mtimeMs;
|
|
2569
|
+
if (file.startsWith("cache-") && file.endsWith(".json")) {
|
|
2570
|
+
cacheFiles.push({ name: file, mtime });
|
|
2571
|
+
} else if (file.startsWith("provider-detect-") && file.endsWith(".json")) {
|
|
2572
|
+
providerDetectFiles.push({ name: file, mtime });
|
|
2573
|
+
} else if (file.endsWith(".tmp")) {
|
|
2574
|
+
tmpFiles.push({ name: file, mtime });
|
|
2575
|
+
}
|
|
2576
|
+
} catch (error) {
|
|
2577
|
+
logger.debug("GC: Failed to stat file, skipping", { file, error: String(error) });
|
|
2197
2578
|
}
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2579
|
+
}
|
|
2580
|
+
const now = Date.now();
|
|
2581
|
+
let deletedCount = 0;
|
|
2582
|
+
for (const file of cacheFiles) {
|
|
2583
|
+
const age = now - file.mtime;
|
|
2584
|
+
if (age > GC_MAX_AGE_MS) {
|
|
2585
|
+
try {
|
|
2586
|
+
unlinkSync3(join6(cacheDir, file.name));
|
|
2587
|
+
deletedCount++;
|
|
2588
|
+
logger.debug("GC: Deleted old cache file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
|
|
2589
|
+
} catch (error) {
|
|
2590
|
+
logger.debug("GC: Failed to delete cache file", { file: file.name, error: String(error) });
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
for (const file of providerDetectFiles) {
|
|
2595
|
+
const age = now - file.mtime;
|
|
2596
|
+
if (age > GC_MAX_AGE_MS) {
|
|
2597
|
+
try {
|
|
2598
|
+
unlinkSync3(join6(cacheDir, file.name));
|
|
2599
|
+
deletedCount++;
|
|
2600
|
+
logger.debug("GC: Deleted old provider-detect file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
logger.debug("GC: Failed to delete provider-detect file", { file: file.name, error: String(error) });
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
for (const file of tmpFiles) {
|
|
2607
|
+
const age = now - file.mtime;
|
|
2608
|
+
if (age > GC_ORPHAN_TMP_AGE_MS) {
|
|
2609
|
+
try {
|
|
2610
|
+
unlinkSync3(join6(cacheDir, file.name));
|
|
2611
|
+
deletedCount++;
|
|
2612
|
+
logger.debug("GC: Deleted orphaned tmp file", { file: file.name, ageMinutes: Math.floor(age / (60 * 1000)) });
|
|
2613
|
+
} catch (error) {
|
|
2614
|
+
logger.debug("GC: Failed to delete tmp file", { file: file.name, error: String(error) });
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
const remainingCacheFiles = cacheFiles.filter((file) => {
|
|
2619
|
+
const age = now - file.mtime;
|
|
2620
|
+
return age <= GC_MAX_AGE_MS;
|
|
2621
|
+
});
|
|
2622
|
+
if (remainingCacheFiles.length > GC_MAX_CACHE_FILES) {
|
|
2623
|
+
remainingCacheFiles.sort((a, b) => a.mtime - b.mtime);
|
|
2624
|
+
const toDelete = remainingCacheFiles.slice(0, remainingCacheFiles.length - GC_MAX_CACHE_FILES);
|
|
2625
|
+
for (const file of toDelete) {
|
|
2626
|
+
try {
|
|
2627
|
+
unlinkSync3(join6(cacheDir, file.name));
|
|
2628
|
+
deletedCount++;
|
|
2629
|
+
logger.debug("GC: Deleted cache file (count limit)", { file: file.name });
|
|
2630
|
+
} catch (error) {
|
|
2631
|
+
logger.debug("GC: Failed to delete cache file", { file: file.name, error: String(error) });
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
logger.debug("GC: Garbage collection completed", { deletedCount });
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
logger.debug("GC: Garbage collection failed", { error: String(error) });
|
|
2200
2638
|
}
|
|
2201
2639
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2640
|
+
|
|
2641
|
+
// src/cli/piped-mode.ts
|
|
2642
|
+
function buildExecutionContext(args, isPiped, startTime) {
|
|
2643
|
+
return (async () => {
|
|
2644
|
+
const env = readCurrentEnv();
|
|
2645
|
+
logger.debug("Environment loaded", {
|
|
2646
|
+
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
2647
|
+
hasToken: !!env.authToken,
|
|
2648
|
+
providerOverride: env.providerOverride,
|
|
2649
|
+
pollIntervalOverride: env.pollIntervalOverride
|
|
2650
|
+
});
|
|
2651
|
+
const envError = validateRequiredEnv(env);
|
|
2652
|
+
if (envError) {
|
|
2653
|
+
const errorOutput = renderError("missing-env", "without-cache");
|
|
2654
|
+
process.stdout.write(errorOutput);
|
|
2655
|
+
process.exit(0);
|
|
2656
|
+
}
|
|
2657
|
+
const baseUrl = env.baseUrl;
|
|
2658
|
+
const authToken = env.authToken;
|
|
2659
|
+
if (!baseUrl || !authToken) {
|
|
2660
|
+
process.exit(1);
|
|
2661
|
+
}
|
|
2662
|
+
const config = loadConfig(args.configPath);
|
|
2663
|
+
const configPath = getConfigPath(args.configPath);
|
|
2664
|
+
const configHash = computeConfigHash(configPath);
|
|
2665
|
+
logger.debug("Config loaded", { configPath, configHash });
|
|
2666
|
+
const probeTimeout = isPiped ? Math.min(1500, Math.max(200, Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) - 200)) : 3000;
|
|
2667
|
+
const providerId = await resolveProvider(baseUrl, env.providerOverride, config.customProviders ?? {}, probeTimeout);
|
|
2668
|
+
const provider = getProvider(providerId, config.customProviders ?? {});
|
|
2669
|
+
logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
2670
|
+
if (!provider) {
|
|
2671
|
+
logger.error("Provider not found", { providerId });
|
|
2672
|
+
const errorOutput = renderError("provider-unknown", "without-cache");
|
|
2673
|
+
process.stdout.write(errorOutput);
|
|
2674
|
+
process.exit(0);
|
|
2675
|
+
}
|
|
2676
|
+
const cachedEntry = readCache(baseUrl);
|
|
2677
|
+
logger.debug("Cache read", {
|
|
2678
|
+
cacheHit: !!cachedEntry,
|
|
2679
|
+
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
2680
|
+
});
|
|
2681
|
+
const timeoutBudgetMs = isPiped ? Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) : 1e4;
|
|
2682
|
+
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? 800, timeoutBudgetMs - 100) : 1e4;
|
|
2683
|
+
const ctx = {
|
|
2684
|
+
env,
|
|
2685
|
+
config,
|
|
2686
|
+
configHash,
|
|
2687
|
+
cachedEntry,
|
|
2688
|
+
providerId,
|
|
2689
|
+
provider,
|
|
2690
|
+
timeoutBudgetMs,
|
|
2691
|
+
startTime,
|
|
2692
|
+
fetchTimeoutMs
|
|
2693
|
+
};
|
|
2694
|
+
return { ctx, baseUrl };
|
|
2695
|
+
})();
|
|
2205
2696
|
}
|
|
2206
|
-
function
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
return false;
|
|
2697
|
+
function formatOutput(output, isPiped) {
|
|
2698
|
+
let normalizedOutput = output;
|
|
2699
|
+
if (!normalizedOutput || normalizedOutput.trim().length === 0) {
|
|
2700
|
+
logger.debug("Empty output detected, using fallback");
|
|
2701
|
+
normalizedOutput = "[loading...]";
|
|
2212
2702
|
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
padding: 0
|
|
2220
|
-
};
|
|
2221
|
-
saveClaudeSettings(settings);
|
|
2222
|
-
}
|
|
2223
|
-
function uninstallStatusLine() {
|
|
2224
|
-
const settings = loadClaudeSettings();
|
|
2225
|
-
if ("statusLine" in settings) {
|
|
2226
|
-
delete settings.statusLine;
|
|
2227
|
-
saveClaudeSettings(settings);
|
|
2703
|
+
if (isPiped) {
|
|
2704
|
+
logger.debug("Output formatted for piped mode (ANSI reset + NBSP)");
|
|
2705
|
+
return "\x1B[0m" + normalizedOutput.replace(/ /g, " ");
|
|
2706
|
+
} else {
|
|
2707
|
+
logger.debug("Output written (TTY mode)");
|
|
2708
|
+
return normalizedOutput;
|
|
2228
2709
|
}
|
|
2229
2710
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
"statusline",
|
|
2256
|
-
"api",
|
|
2257
|
-
"usage",
|
|
2258
|
-
"monitoring",
|
|
2259
|
-
"tui",
|
|
2260
|
-
"cli"
|
|
2261
|
-
],
|
|
2262
|
-
author: "Liafonx",
|
|
2263
|
-
license: "MIT",
|
|
2264
|
-
repository: {
|
|
2265
|
-
type: "git",
|
|
2266
|
-
url: "git+https://github.com/liafonx/cc-api-statusline.git"
|
|
2267
|
-
},
|
|
2268
|
-
homepage: "https://github.com/liafonx/cc-api-statusline#readme",
|
|
2269
|
-
bugs: {
|
|
2270
|
-
url: "https://github.com/liafonx/cc-api-statusline/issues"
|
|
2271
|
-
},
|
|
2272
|
-
dependencies: {},
|
|
2273
|
-
devDependencies: {
|
|
2274
|
-
"@eslint/js": "^9.17.0",
|
|
2275
|
-
"@types/bun": "^1.1.14",
|
|
2276
|
-
eslint: "^9.17.0",
|
|
2277
|
-
typescript: "^5.7.2",
|
|
2278
|
-
"typescript-eslint": "^8.18.2",
|
|
2279
|
-
vitest: "^2.1.8"
|
|
2280
|
-
},
|
|
2281
|
-
engines: {
|
|
2282
|
-
node: ">=18.0.0"
|
|
2283
|
-
},
|
|
2284
|
-
publishConfig: {
|
|
2285
|
-
provenance: true
|
|
2286
|
-
}
|
|
2287
|
-
};
|
|
2288
|
-
|
|
2289
|
-
// src/main.ts
|
|
2290
|
-
function parseArgs() {
|
|
2291
|
-
const args = process.argv.slice(2);
|
|
2292
|
-
let help = false;
|
|
2293
|
-
let version = false;
|
|
2294
|
-
let once = false;
|
|
2295
|
-
let install = false;
|
|
2296
|
-
let uninstall = false;
|
|
2297
|
-
let force = false;
|
|
2298
|
-
let configPath;
|
|
2299
|
-
let runner;
|
|
2300
|
-
for (let i = 0;i < args.length; i++) {
|
|
2301
|
-
const arg = args[i];
|
|
2302
|
-
if (arg === "--help" || arg === "-h") {
|
|
2303
|
-
help = true;
|
|
2304
|
-
} else if (arg === "--version" || arg === "-v") {
|
|
2305
|
-
version = true;
|
|
2306
|
-
} else if (arg === "--once") {
|
|
2307
|
-
once = true;
|
|
2308
|
-
} else if (arg === "--install") {
|
|
2309
|
-
install = true;
|
|
2310
|
-
} else if (arg === "--uninstall") {
|
|
2311
|
-
uninstall = true;
|
|
2312
|
-
} else if (arg === "--force") {
|
|
2313
|
-
force = true;
|
|
2314
|
-
} else if (arg === "--config" && i + 1 < args.length) {
|
|
2315
|
-
configPath = args[i + 1];
|
|
2316
|
-
i++;
|
|
2317
|
-
} else if (arg === "--runner" && i + 1 < args.length) {
|
|
2318
|
-
const nextArg = args[i + 1];
|
|
2319
|
-
if (nextArg === "npx" || nextArg === "bunx") {
|
|
2320
|
-
runner = nextArg;
|
|
2321
|
-
}
|
|
2322
|
-
i++;
|
|
2323
|
-
}
|
|
2711
|
+
async function executePipedMode(args) {
|
|
2712
|
+
const startTime = Date.now();
|
|
2713
|
+
logger.debug("=== cc-api-statusline execution started ===");
|
|
2714
|
+
logger.debug("Start time", { startTime });
|
|
2715
|
+
const isPiped = !process.stdin.isTTY;
|
|
2716
|
+
logger.debug("Mode detection", { isPiped, once: args.once });
|
|
2717
|
+
const { ctx, baseUrl } = await buildExecutionContext(args, isPiped, startTime);
|
|
2718
|
+
logger.debug("Execution context prepared", {
|
|
2719
|
+
timeoutBudgetMs: ctx.timeoutBudgetMs,
|
|
2720
|
+
fetchTimeoutMs: ctx.fetchTimeoutMs
|
|
2721
|
+
});
|
|
2722
|
+
const result = await executeCycle(ctx);
|
|
2723
|
+
const executionTime = Date.now() - startTime;
|
|
2724
|
+
logger.debug("Execution completed", {
|
|
2725
|
+
exitCode: result.exitCode,
|
|
2726
|
+
executionTime: `${executionTime}ms`,
|
|
2727
|
+
outputLength: result.output.length,
|
|
2728
|
+
cacheUpdate: !!result.cacheUpdate
|
|
2729
|
+
});
|
|
2730
|
+
const formattedOutput = formatOutput(result.output, isPiped);
|
|
2731
|
+
process.stdout.write(formattedOutput);
|
|
2732
|
+
if (result.cacheUpdate) {
|
|
2733
|
+
writeCache(baseUrl, result.cacheUpdate);
|
|
2734
|
+
logger.debug("Cache written", { baseUrl });
|
|
2735
|
+
runCacheGC(getCacheDir());
|
|
2324
2736
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
function showHelp() {
|
|
2328
|
-
console.log(`
|
|
2329
|
-
cc-api-statusline — Claude API statusline widget
|
|
2330
|
-
|
|
2331
|
-
Usage:
|
|
2332
|
-
cc-api-statusline [options]
|
|
2333
|
-
|
|
2334
|
-
Options:
|
|
2335
|
-
--help, -h Show this help message
|
|
2336
|
-
--version, -v Show version
|
|
2337
|
-
--once Fetch once and exit (no polling)
|
|
2338
|
-
--config <path> Use custom config file
|
|
2339
|
-
--install Register as Claude Code statusline widget
|
|
2340
|
-
--uninstall Remove statusline widget registration
|
|
2341
|
-
--runner <runner> Package runner: npx or bunx (default: auto-detect)
|
|
2342
|
-
--force Force overwrite existing statusline configuration
|
|
2343
|
-
|
|
2344
|
-
Environment Variables:
|
|
2345
|
-
ANTHROPIC_BASE_URL API endpoint (required)
|
|
2346
|
-
ANTHROPIC_AUTH_TOKEN API key (required)
|
|
2347
|
-
CC_STATUSLINE_PROVIDER Override provider detection
|
|
2348
|
-
CC_STATUSLINE_POLL Override poll interval (seconds)
|
|
2349
|
-
CC_STATUSLINE_TIMEOUT Piped mode timeout (milliseconds, default 1000)
|
|
2350
|
-
DEBUG or CC_STATUSLINE_DEBUG Enable debug logging to ~/.claude/cc-api-statusline/debug.log
|
|
2351
|
-
|
|
2352
|
-
Config File:
|
|
2353
|
-
~/.claude/cc-api-statusline/config.json
|
|
2354
|
-
|
|
2355
|
-
Documentation:
|
|
2356
|
-
https://github.com/liafonx/cc-api-statusline
|
|
2357
|
-
`.trim());
|
|
2358
|
-
}
|
|
2359
|
-
function showVersion() {
|
|
2360
|
-
console.log(`cc-api-statusline v${package_default.version}`);
|
|
2737
|
+
logger.debug("=== cc-api-statusline execution completed ===");
|
|
2738
|
+
process.exit(result.exitCode);
|
|
2361
2739
|
}
|
|
2740
|
+
// src/main.ts
|
|
2362
2741
|
function discardStdin() {
|
|
2363
2742
|
if (!process.stdin.isTTY) {
|
|
2364
2743
|
process.stdin.resume();
|
|
@@ -2366,9 +2745,7 @@ function discardStdin() {
|
|
|
2366
2745
|
}
|
|
2367
2746
|
}
|
|
2368
2747
|
async function main() {
|
|
2369
|
-
|
|
2370
|
-
logger.debug("=== cc-api-statusline execution started ===");
|
|
2371
|
-
logger.debug("Start time", { startTime, version: package_default.version });
|
|
2748
|
+
logger.debug("=== cc-api-statusline execution started ===", { version: package_default.version });
|
|
2372
2749
|
discardStdin();
|
|
2373
2750
|
const args = parseArgs();
|
|
2374
2751
|
logger.debug("Parsed arguments", { args });
|
|
@@ -2381,119 +2758,22 @@ async function main() {
|
|
|
2381
2758
|
process.exit(0);
|
|
2382
2759
|
}
|
|
2383
2760
|
if (args.install) {
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
console.error("Error: statusLine is already configured in settings.json");
|
|
2387
|
-
console.error(`Current command: ${existing}`);
|
|
2388
|
-
console.error("Use --force to overwrite, or --uninstall to remove first.");
|
|
2389
|
-
process.exit(1);
|
|
2390
|
-
}
|
|
2391
|
-
const runner = args.runner ?? (isBunxAvailable() ? "bunx" : "npx");
|
|
2392
|
-
installStatusLine(runner);
|
|
2393
|
-
console.log("✓ Statusline installed successfully!");
|
|
2394
|
-
console.log(` Runner: ${runner}`);
|
|
2395
|
-
console.log(` Command: ${runner} -y cc-api-statusline@latest`);
|
|
2396
|
-
console.log(` Config: ~/.claude/settings.json`);
|
|
2397
|
-
process.exit(0);
|
|
2761
|
+
handleInstall(args);
|
|
2762
|
+
return;
|
|
2398
2763
|
}
|
|
2399
2764
|
if (args.uninstall) {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
console.log("No statusLine configuration found in settings.json");
|
|
2403
|
-
process.exit(0);
|
|
2404
|
-
}
|
|
2405
|
-
uninstallStatusLine();
|
|
2406
|
-
console.log("✓ Statusline uninstalled successfully");
|
|
2407
|
-
console.log(" Removed statusLine from ~/.claude/settings.json");
|
|
2408
|
-
process.exit(0);
|
|
2765
|
+
handleUninstall();
|
|
2766
|
+
return;
|
|
2409
2767
|
}
|
|
2410
|
-
|
|
2411
|
-
logger.debug("Mode detection", { isPiped, once: args.once });
|
|
2412
|
-
if (!isPiped && !args.once) {
|
|
2768
|
+
if (process.stdin.isTTY && !args.once) {
|
|
2413
2769
|
console.log("Interactive configuration mode coming soon.");
|
|
2414
2770
|
console.log("Use --once for a single fetch, or configure as a Claude Code statusline command.");
|
|
2415
2771
|
process.exit(0);
|
|
2416
2772
|
}
|
|
2417
|
-
|
|
2418
|
-
logger.debug("Environment loaded", {
|
|
2419
|
-
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
2420
|
-
hasToken: !!env.authToken,
|
|
2421
|
-
providerOverride: env.providerOverride,
|
|
2422
|
-
pollIntervalOverride: env.pollIntervalOverride
|
|
2423
|
-
});
|
|
2424
|
-
const envError = validateRequiredEnv(env);
|
|
2425
|
-
if (envError) {
|
|
2426
|
-
const errorOutput = renderError("missing-env", "without-cache");
|
|
2427
|
-
process.stdout.write(errorOutput);
|
|
2428
|
-
process.exit(0);
|
|
2429
|
-
}
|
|
2430
|
-
const baseUrl = env.baseUrl;
|
|
2431
|
-
const authToken = env.authToken;
|
|
2432
|
-
if (!baseUrl || !authToken) {
|
|
2433
|
-
process.exit(1);
|
|
2434
|
-
}
|
|
2435
|
-
const config = loadConfig(args.configPath);
|
|
2436
|
-
const configPath = getConfigPath(args.configPath);
|
|
2437
|
-
const configHash = computeConfigHash(configPath);
|
|
2438
|
-
logger.debug("Config loaded", { configPath, configHash });
|
|
2439
|
-
const probeTimeout = isPiped ? Math.min(1500, Math.max(200, Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) - 200)) : 3000;
|
|
2440
|
-
const providerId = await resolveProvider(baseUrl, env.providerOverride, config.customProviders ?? {}, probeTimeout);
|
|
2441
|
-
const provider = getProvider(providerId, config.customProviders ?? {});
|
|
2442
|
-
logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
2443
|
-
if (!provider) {
|
|
2444
|
-
logger.error("Provider not found", { providerId });
|
|
2445
|
-
const errorOutput = renderError("provider-unknown", "without-cache");
|
|
2446
|
-
process.stdout.write(errorOutput);
|
|
2447
|
-
process.exit(0);
|
|
2448
|
-
}
|
|
2449
|
-
const cachedEntry = readCache(baseUrl);
|
|
2450
|
-
logger.debug("Cache read", {
|
|
2451
|
-
cacheHit: !!cachedEntry,
|
|
2452
|
-
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
2453
|
-
});
|
|
2454
|
-
const timeoutBudgetMs = isPiped ? Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) : 1e4;
|
|
2455
|
-
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? 800, timeoutBudgetMs - 100) : 1e4;
|
|
2456
|
-
const ctx = {
|
|
2457
|
-
env,
|
|
2458
|
-
config,
|
|
2459
|
-
configHash,
|
|
2460
|
-
cachedEntry,
|
|
2461
|
-
providerId,
|
|
2462
|
-
provider,
|
|
2463
|
-
timeoutBudgetMs,
|
|
2464
|
-
startTime,
|
|
2465
|
-
fetchTimeoutMs
|
|
2466
|
-
};
|
|
2467
|
-
logger.debug("Execution context prepared", { timeoutBudgetMs, fetchTimeoutMs });
|
|
2468
|
-
const result = await executeCycle(ctx);
|
|
2469
|
-
const executionTime = Date.now() - startTime;
|
|
2470
|
-
logger.debug("Execution completed", {
|
|
2471
|
-
exitCode: result.exitCode,
|
|
2472
|
-
executionTime: `${executionTime}ms`,
|
|
2473
|
-
outputLength: result.output.length,
|
|
2474
|
-
cacheUpdate: !!result.cacheUpdate
|
|
2475
|
-
});
|
|
2476
|
-
let output = result.output;
|
|
2477
|
-
if (!output || output.trim().length === 0) {
|
|
2478
|
-
output = "[loading...]";
|
|
2479
|
-
logger.debug("Empty output detected, using fallback");
|
|
2480
|
-
}
|
|
2481
|
-
if (isPiped) {
|
|
2482
|
-
const formatted = "\x1B[0m" + output.replace(/ /g, " ");
|
|
2483
|
-
process.stdout.write(formatted);
|
|
2484
|
-
logger.debug("Output formatted for piped mode (ANSI reset + NBSP)");
|
|
2485
|
-
} else {
|
|
2486
|
-
process.stdout.write(output);
|
|
2487
|
-
logger.debug("Output written (TTY mode)");
|
|
2488
|
-
}
|
|
2489
|
-
if (result.cacheUpdate) {
|
|
2490
|
-
writeCache(baseUrl, result.cacheUpdate);
|
|
2491
|
-
logger.debug("Cache updated");
|
|
2492
|
-
}
|
|
2493
|
-
logger.debug("=== Execution finished ===", { exitCode: result.exitCode });
|
|
2494
|
-
process.exit(result.exitCode);
|
|
2773
|
+
await executePipedMode(args);
|
|
2495
2774
|
}
|
|
2496
2775
|
main().catch((error) => {
|
|
2497
|
-
|
|
2776
|
+
logger.error("Unhandled error in main", { error: String(error) });
|
|
2777
|
+
console.error(`Fatal error: ${error}`);
|
|
2498
2778
|
process.exit(1);
|
|
2499
2779
|
});
|