cc-api-statusline 0.1.3 → 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 +1142 -858
- 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,24 +923,33 @@ function detectClaudeVersion() {
|
|
|
697
923
|
}
|
|
698
924
|
}
|
|
699
925
|
|
|
700
|
-
// src/providers/
|
|
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
|
}
|
|
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
|
+
|
|
952
|
+
// src/providers/sub2api.ts
|
|
718
953
|
function mapPeriodTokens(data) {
|
|
719
954
|
if (!data)
|
|
720
955
|
return null;
|
|
@@ -728,108 +963,66 @@ function mapPeriodTokens(data) {
|
|
|
728
963
|
cost: data.cost ?? 0
|
|
729
964
|
};
|
|
730
965
|
}
|
|
731
|
-
function
|
|
732
|
-
if (used === undefined)
|
|
733
|
-
return null;
|
|
734
|
-
if (limit === null || limit === undefined)
|
|
735
|
-
return null;
|
|
736
|
-
const remaining = Math.max(0, limit - used);
|
|
737
|
-
return {
|
|
738
|
-
used,
|
|
739
|
-
limit,
|
|
740
|
-
remaining,
|
|
741
|
-
resetsAt
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
async function fetchSub2api(baseUrl, token, config, timeoutMs = 5000) {
|
|
966
|
+
async function fetchSub2api(baseUrl, token, config, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
745
967
|
const url = `${baseUrl}/v1/usage`;
|
|
746
968
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
747
969
|
if (resolvedUA) {
|
|
748
970
|
logger.debug(`Using User-Agent: ${resolvedUA}`);
|
|
749
971
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
Accept: "application/json"
|
|
756
|
-
}
|
|
757
|
-
}, timeoutMs, resolvedUA);
|
|
758
|
-
const data = JSON.parse(responseText);
|
|
759
|
-
const hasSubscription = !!data.subscription;
|
|
760
|
-
const billingMode = hasSubscription ? "subscription" : "balance";
|
|
761
|
-
const result = {
|
|
762
|
-
provider: "sub2api",
|
|
763
|
-
billingMode,
|
|
764
|
-
planName: data.planName ?? "Unknown",
|
|
765
|
-
fetchedAt: new Date().toISOString(),
|
|
766
|
-
resetSemantics: "end-of-day",
|
|
767
|
-
daily: null,
|
|
768
|
-
weekly: null,
|
|
769
|
-
monthly: null,
|
|
770
|
-
balance: null,
|
|
771
|
-
resetsAt: null,
|
|
772
|
-
tokenStats: null,
|
|
773
|
-
rateLimit: null
|
|
774
|
-
};
|
|
775
|
-
if (billingMode === "balance") {
|
|
776
|
-
const remaining = data.remaining ?? 0;
|
|
777
|
-
if (remaining === -1) {
|
|
778
|
-
result.balance = {
|
|
779
|
-
remaining: -1,
|
|
780
|
-
initial: null,
|
|
781
|
-
unit: data.unit ?? "USD"
|
|
782
|
-
};
|
|
783
|
-
} else {
|
|
784
|
-
result.balance = {
|
|
785
|
-
remaining,
|
|
786
|
-
initial: null,
|
|
787
|
-
unit: data.unit ?? "USD"
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
} else {
|
|
791
|
-
const sub = data.subscription;
|
|
792
|
-
if (!sub) {
|
|
793
|
-
throw new Error("Subscription mode but no subscription object in response");
|
|
794
|
-
}
|
|
795
|
-
result.daily = createQuotaWindow(sub.daily_usage_usd, sub.daily_limit_usd, computeNextMidnightLocal());
|
|
796
|
-
result.weekly = createQuotaWindow(sub.weekly_usage_usd, sub.weekly_limit_usd, computeNextMondayLocal());
|
|
797
|
-
result.monthly = createQuotaWindow(sub.monthly_usage_usd, sub.monthly_limit_usd, computeFirstOfNextMonthLocal());
|
|
798
|
-
result.resetsAt = computeSoonestReset(result);
|
|
799
|
-
}
|
|
800
|
-
if (data.usage) {
|
|
801
|
-
result.tokenStats = {
|
|
802
|
-
today: mapPeriodTokens(data.usage.today),
|
|
803
|
-
total: mapPeriodTokens(data.usage.total),
|
|
804
|
-
rpm: data.usage.rpm ?? null,
|
|
805
|
-
tpm: data.usage.tpm ?? null
|
|
806
|
-
};
|
|
972
|
+
const responseText = await secureFetch(url, {
|
|
973
|
+
method: "GET",
|
|
974
|
+
headers: {
|
|
975
|
+
Authorization: `Bearer ${token}`,
|
|
976
|
+
Accept: "application/json"
|
|
807
977
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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");
|
|
830
1004
|
}
|
|
831
|
-
|
|
832
|
-
|
|
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
|
+
};
|
|
833
1026
|
}
|
|
834
1027
|
|
|
835
1028
|
// src/providers/health-probe.ts
|
|
@@ -870,6 +1063,13 @@ async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
|
870
1063
|
}
|
|
871
1064
|
}
|
|
872
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
|
+
|
|
873
1073
|
// src/providers/claude-relay-service.ts
|
|
874
1074
|
function computeWeeklyResetTime(resetDay, resetHour) {
|
|
875
1075
|
const now = new Date;
|
|
@@ -882,20 +1082,7 @@ function computeWeeklyResetTime(resetDay, resetHour) {
|
|
|
882
1082
|
const resetDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilReset, resetHour, 0, 0, 0));
|
|
883
1083
|
return resetDate.toISOString();
|
|
884
1084
|
}
|
|
885
|
-
function
|
|
886
|
-
if (used === undefined)
|
|
887
|
-
return null;
|
|
888
|
-
if (!limit || limit <= 0)
|
|
889
|
-
return null;
|
|
890
|
-
const remaining = Math.max(0, limit - used);
|
|
891
|
-
return {
|
|
892
|
-
used,
|
|
893
|
-
limit,
|
|
894
|
-
remaining,
|
|
895
|
-
resetsAt
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000) {
|
|
1085
|
+
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
899
1086
|
const origin = extractOrigin(baseUrl);
|
|
900
1087
|
const url = `${origin}/apiStats/api/user-stats`;
|
|
901
1088
|
const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
|
|
@@ -911,67 +1098,65 @@ async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000)
|
|
|
911
1098
|
body: JSON.stringify({ apiKey: token })
|
|
912
1099
|
}, timeoutMs, resolvedUA);
|
|
913
1100
|
const response = JSON.parse(responseText);
|
|
1101
|
+
if (!response || typeof response !== "object") {
|
|
1102
|
+
throw new Error("Invalid response: expected object");
|
|
1103
|
+
}
|
|
914
1104
|
if (!response.success) {
|
|
915
1105
|
throw new HttpError("Relay API returned success: false");
|
|
916
1106
|
}
|
|
917
1107
|
const data = response.data;
|
|
1108
|
+
if (!data || typeof data !== "object") {
|
|
1109
|
+
throw new Error("Invalid response: missing data object");
|
|
1110
|
+
}
|
|
918
1111
|
const limits = data.limits;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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,
|
|
924
1149
|
resetSemantics: "rolling-window",
|
|
925
|
-
daily
|
|
926
|
-
weekly
|
|
927
|
-
monthly
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
rateLimit: null
|
|
1150
|
+
daily,
|
|
1151
|
+
weekly,
|
|
1152
|
+
monthly,
|
|
1153
|
+
resetsAt,
|
|
1154
|
+
tokenStats,
|
|
1155
|
+
rateLimit
|
|
932
1156
|
};
|
|
933
|
-
result.daily = createQuotaWindow2(limits.currentDailyCost, limits.dailyCostLimit, null);
|
|
934
|
-
if (limits.weeklyResetDay !== undefined && limits.weeklyResetHour !== undefined) {
|
|
935
|
-
const weeklyResetsAt = computeWeeklyResetTime(limits.weeklyResetDay, limits.weeklyResetHour);
|
|
936
|
-
result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, weeklyResetsAt);
|
|
937
|
-
} else {
|
|
938
|
-
result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, null);
|
|
939
|
-
}
|
|
940
|
-
result.monthly = null;
|
|
941
|
-
if (limits.windowEndTime) {
|
|
942
|
-
result.resetsAt = new Date(limits.windowEndTime).toISOString();
|
|
943
|
-
}
|
|
944
|
-
if (data.usage?.total) {
|
|
945
|
-
const total = data.usage.total;
|
|
946
|
-
result.tokenStats = {
|
|
947
|
-
today: null,
|
|
948
|
-
total: {
|
|
949
|
-
requests: total.requests ?? 0,
|
|
950
|
-
inputTokens: total.inputTokens ?? 0,
|
|
951
|
-
outputTokens: total.outputTokens ?? 0,
|
|
952
|
-
cacheCreationTokens: total.cacheCreateTokens ?? 0,
|
|
953
|
-
cacheReadTokens: total.cacheReadTokens ?? 0,
|
|
954
|
-
totalTokens: total.tokens ?? (total.inputTokens ?? 0) + (total.outputTokens ?? 0),
|
|
955
|
-
cost: total.cost ?? 0
|
|
956
|
-
},
|
|
957
|
-
rpm: null,
|
|
958
|
-
tpm: null
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
if (limits.rateLimitWindow !== undefined) {
|
|
962
|
-
result.rateLimit = {
|
|
963
|
-
windowSeconds: limits.rateLimitWindow * 60,
|
|
964
|
-
requestsUsed: limits.currentWindowRequests ?? 0,
|
|
965
|
-
requestsLimit: limits.rateLimitRequests && limits.rateLimitRequests > 0 ? limits.rateLimitRequests : null,
|
|
966
|
-
costUsed: limits.currentWindowCost ?? 0,
|
|
967
|
-
costLimit: limits.rateLimitCost && limits.rateLimitCost > 0 ? limits.rateLimitCost : null,
|
|
968
|
-
remainingSeconds: limits.windowRemainingSeconds ?? 0
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
return result;
|
|
972
1157
|
}
|
|
973
1158
|
|
|
974
|
-
// src/providers/custom.ts
|
|
1159
|
+
// src/providers/custom-mapping.ts
|
|
975
1160
|
function resolveJsonPath(data, path) {
|
|
976
1161
|
if (!path.startsWith("$.")) {
|
|
977
1162
|
return path;
|
|
@@ -1016,6 +1201,107 @@ function extractString(data, mapping, defaultValue = "") {
|
|
|
1016
1201
|
return value;
|
|
1017
1202
|
return String(value);
|
|
1018
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
|
|
1019
1305
|
function validateCustomProvider(providerConfig) {
|
|
1020
1306
|
if (!providerConfig.id)
|
|
1021
1307
|
return "Custom provider missing required field: id";
|
|
@@ -1044,7 +1330,7 @@ function validateCustomProvider(providerConfig) {
|
|
|
1044
1330
|
}
|
|
1045
1331
|
return null;
|
|
1046
1332
|
}
|
|
1047
|
-
async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs =
|
|
1333
|
+
async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
1048
1334
|
const validationError = validateCustomProvider(providerConfig);
|
|
1049
1335
|
if (validationError) {
|
|
1050
1336
|
throw new Error(`Invalid custom provider providerConfig: ${validationError}`);
|
|
@@ -1083,117 +1369,7 @@ async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs
|
|
|
1083
1369
|
body
|
|
1084
1370
|
}, timeoutMs, resolvedUA);
|
|
1085
1371
|
const responseData = JSON.parse(responseText);
|
|
1086
|
-
const
|
|
1087
|
-
const billingModeStr = extractString(responseData, mapping.billingMode, "subscription");
|
|
1088
|
-
const billingMode = billingModeStr === "balance" ? "balance" : "subscription";
|
|
1089
|
-
const planName = extractString(responseData, mapping.planName, providerConfig.displayName ?? providerConfig.id);
|
|
1090
|
-
const result = createEmptyNormalizedUsage(providerConfig.id, billingMode, planName);
|
|
1091
|
-
result.resetSemantics = billingMode === "balance" ? "expiry" : "end-of-day";
|
|
1092
|
-
if (mapping["balance.remaining"]) {
|
|
1093
|
-
const remaining = extractNumber(responseData, mapping["balance.remaining"]);
|
|
1094
|
-
if (remaining !== null) {
|
|
1095
|
-
result.balance = {
|
|
1096
|
-
remaining,
|
|
1097
|
-
initial: extractNumber(responseData, mapping["balance.initial"]),
|
|
1098
|
-
unit: extractString(responseData, mapping["balance.unit"], "USD")
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
const dailyUsed = extractNumber(responseData, mapping["daily.used"]);
|
|
1103
|
-
const dailyLimitRaw = extractNumber(responseData, mapping["daily.limit"]);
|
|
1104
|
-
const dailyLimit = dailyLimitRaw === 0 ? null : dailyLimitRaw;
|
|
1105
|
-
if (dailyUsed !== null) {
|
|
1106
|
-
result.daily = {
|
|
1107
|
-
used: dailyUsed,
|
|
1108
|
-
limit: dailyLimit,
|
|
1109
|
-
remaining: dailyLimit !== null ? Math.max(0, dailyLimit - dailyUsed) : null,
|
|
1110
|
-
resetsAt: extractString(responseData, mapping["daily.resetsAt"], "") || null
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
const weeklyUsed = extractNumber(responseData, mapping["weekly.used"]);
|
|
1114
|
-
const weeklyLimitRaw = extractNumber(responseData, mapping["weekly.limit"]);
|
|
1115
|
-
const weeklyLimit = weeklyLimitRaw === 0 ? null : weeklyLimitRaw;
|
|
1116
|
-
if (weeklyUsed !== null) {
|
|
1117
|
-
result.weekly = {
|
|
1118
|
-
used: weeklyUsed,
|
|
1119
|
-
limit: weeklyLimit,
|
|
1120
|
-
remaining: weeklyLimit !== null ? Math.max(0, weeklyLimit - weeklyUsed) : null,
|
|
1121
|
-
resetsAt: extractString(responseData, mapping["weekly.resetsAt"], "") || null
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
const monthlyUsed = extractNumber(responseData, mapping["monthly.used"]);
|
|
1125
|
-
const monthlyLimitRaw = extractNumber(responseData, mapping["monthly.limit"]);
|
|
1126
|
-
const monthlyLimit = monthlyLimitRaw === 0 ? null : monthlyLimitRaw;
|
|
1127
|
-
if (monthlyUsed !== null) {
|
|
1128
|
-
result.monthly = {
|
|
1129
|
-
used: monthlyUsed,
|
|
1130
|
-
limit: monthlyLimit,
|
|
1131
|
-
remaining: monthlyLimit !== null ? Math.max(0, monthlyLimit - monthlyUsed) : null,
|
|
1132
|
-
resetsAt: extractString(responseData, mapping["monthly.resetsAt"], "") || null
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
if (mapping["tokenStats.today.requests"]) {
|
|
1136
|
-
const todayRequests = extractNumber(responseData, mapping["tokenStats.today.requests"]);
|
|
1137
|
-
if (todayRequests !== null) {
|
|
1138
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1139
|
-
result.tokenStats.today = {
|
|
1140
|
-
requests: todayRequests,
|
|
1141
|
-
inputTokens: extractNumber(responseData, mapping["tokenStats.today.inputTokens"]) ?? 0,
|
|
1142
|
-
outputTokens: extractNumber(responseData, mapping["tokenStats.today.outputTokens"]) ?? 0,
|
|
1143
|
-
cacheCreationTokens: extractNumber(responseData, mapping["tokenStats.today.cacheCreationTokens"]) ?? 0,
|
|
1144
|
-
cacheReadTokens: extractNumber(responseData, mapping["tokenStats.today.cacheReadTokens"]) ?? 0,
|
|
1145
|
-
totalTokens: extractNumber(responseData, mapping["tokenStats.today.totalTokens"]) ?? 0,
|
|
1146
|
-
cost: extractNumber(responseData, mapping["tokenStats.today.cost"]) ?? 0
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
if (mapping["tokenStats.total.requests"]) {
|
|
1151
|
-
const totalRequests = extractNumber(responseData, mapping["tokenStats.total.requests"]);
|
|
1152
|
-
if (totalRequests !== null) {
|
|
1153
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1154
|
-
result.tokenStats.total = {
|
|
1155
|
-
requests: totalRequests,
|
|
1156
|
-
inputTokens: extractNumber(responseData, mapping["tokenStats.total.inputTokens"]) ?? 0,
|
|
1157
|
-
outputTokens: extractNumber(responseData, mapping["tokenStats.total.outputTokens"]) ?? 0,
|
|
1158
|
-
cacheCreationTokens: extractNumber(responseData, mapping["tokenStats.total.cacheCreationTokens"]) ?? 0,
|
|
1159
|
-
cacheReadTokens: extractNumber(responseData, mapping["tokenStats.total.cacheReadTokens"]) ?? 0,
|
|
1160
|
-
totalTokens: extractNumber(responseData, mapping["tokenStats.total.totalTokens"]) ?? 0,
|
|
1161
|
-
cost: extractNumber(responseData, mapping["tokenStats.total.cost"]) ?? 0
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
if (mapping["tokenStats.rpm"]) {
|
|
1166
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1167
|
-
result.tokenStats.rpm = extractNumber(responseData, mapping["tokenStats.rpm"]);
|
|
1168
|
-
}
|
|
1169
|
-
if (mapping["tokenStats.tpm"]) {
|
|
1170
|
-
result.tokenStats = result.tokenStats ?? { today: null, total: null, rpm: null, tpm: null };
|
|
1171
|
-
result.tokenStats.tpm = extractNumber(responseData, mapping["tokenStats.tpm"]);
|
|
1172
|
-
}
|
|
1173
|
-
if (mapping["rateLimit.windowSeconds"]) {
|
|
1174
|
-
const windowSeconds = extractNumber(responseData, mapping["rateLimit.windowSeconds"]);
|
|
1175
|
-
if (windowSeconds !== null) {
|
|
1176
|
-
result.rateLimit = {
|
|
1177
|
-
windowSeconds,
|
|
1178
|
-
requestsUsed: extractNumber(responseData, mapping["rateLimit.requestsUsed"]) ?? 0,
|
|
1179
|
-
requestsLimit: extractNumber(responseData, mapping["rateLimit.requestsLimit"]),
|
|
1180
|
-
costUsed: extractNumber(responseData, mapping["rateLimit.costUsed"]) ?? 0,
|
|
1181
|
-
costLimit: extractNumber(responseData, mapping["rateLimit.costLimit"]),
|
|
1182
|
-
remainingSeconds: extractNumber(responseData, mapping["rateLimit.remainingSeconds"]) ?? 0
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
if (result.daily?.resetsAt || result.weekly?.resetsAt || result.monthly?.resetsAt) {
|
|
1187
|
-
const times = [];
|
|
1188
|
-
if (result.daily?.resetsAt)
|
|
1189
|
-
times.push(result.daily.resetsAt);
|
|
1190
|
-
if (result.weekly?.resetsAt)
|
|
1191
|
-
times.push(result.weekly.resetsAt);
|
|
1192
|
-
if (result.monthly?.resetsAt)
|
|
1193
|
-
times.push(result.monthly.resetsAt);
|
|
1194
|
-
times.sort();
|
|
1195
|
-
result.resetsAt = times[0] ?? null;
|
|
1196
|
-
}
|
|
1372
|
+
const result = mapResponseToUsage(responseData, providerConfig.responseMapping, providerConfig);
|
|
1197
1373
|
return result;
|
|
1198
1374
|
}
|
|
1199
1375
|
|
|
@@ -1323,13 +1499,25 @@ var ANSI_COLORS = {
|
|
|
1323
1499
|
};
|
|
1324
1500
|
var ANSI_RESET = "\x1B[0m";
|
|
1325
1501
|
var ANSI_DIM = "\x1B[2m";
|
|
1326
|
-
function ansiColor(text, color) {
|
|
1502
|
+
function ansiColor(text, color, capabilities) {
|
|
1327
1503
|
if (!color)
|
|
1328
1504
|
return text;
|
|
1329
1505
|
if (ANSI_COLORS[color.toLowerCase()]) {
|
|
1330
1506
|
return `${ANSI_COLORS[color.toLowerCase()]}${text}${ANSI_RESET}`;
|
|
1331
1507
|
}
|
|
1332
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
|
+
}
|
|
1333
1521
|
const rgb = hexToRgb(color);
|
|
1334
1522
|
if (rgb) {
|
|
1335
1523
|
return `\x1B[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}${ANSI_RESET}`;
|
|
@@ -1337,6 +1525,43 @@ function ansiColor(text, color) {
|
|
|
1337
1525
|
}
|
|
1338
1526
|
return text;
|
|
1339
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
|
+
}
|
|
1340
1565
|
function hexToRgb(hex) {
|
|
1341
1566
|
const cleanHex = hex.replace(/^#/, "");
|
|
1342
1567
|
if (cleanHex.length === 3) {
|
|
@@ -1391,10 +1616,14 @@ function resolveColorAlias(alias, usagePercent) {
|
|
|
1391
1616
|
}
|
|
1392
1617
|
}
|
|
1393
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
|
+
|
|
1394
1624
|
// src/renderer/error.ts
|
|
1395
1625
|
function renderError(errorState, mode, provider, message, cacheAge) {
|
|
1396
|
-
|
|
1397
|
-
if (isTransition) {
|
|
1626
|
+
if (isTransitionState(errorState)) {
|
|
1398
1627
|
return renderTransitionState(errorState);
|
|
1399
1628
|
}
|
|
1400
1629
|
if (mode === "without-cache") {
|
|
@@ -1428,11 +1657,13 @@ function renderStandaloneError(errorState, provider, message) {
|
|
|
1428
1657
|
case "auth-error":
|
|
1429
1658
|
return `${warningIcon} Auth error`;
|
|
1430
1659
|
case "rate-limited":
|
|
1431
|
-
return `${warningIcon}
|
|
1660
|
+
return `${warningIcon} Usage limit reached`;
|
|
1432
1661
|
case "provider-unknown":
|
|
1433
1662
|
return `${warningIcon} Unknown provider`;
|
|
1434
1663
|
case "missing-env":
|
|
1435
1664
|
return `${warningIcon} Set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`;
|
|
1665
|
+
case "timeout":
|
|
1666
|
+
return `${warningIcon} Fetching...`;
|
|
1436
1667
|
case "network-error":
|
|
1437
1668
|
case "server-error":
|
|
1438
1669
|
case "parse-error":
|
|
@@ -1456,7 +1687,9 @@ function renderErrorIndicator(errorState, cacheAge) {
|
|
|
1456
1687
|
case "parse-error":
|
|
1457
1688
|
return "[parse error]";
|
|
1458
1689
|
case "rate-limited":
|
|
1459
|
-
return "[
|
|
1690
|
+
return "[limit reached]";
|
|
1691
|
+
case "timeout":
|
|
1692
|
+
return "[timeout]";
|
|
1460
1693
|
default:
|
|
1461
1694
|
return "[error]";
|
|
1462
1695
|
}
|
|
@@ -1467,7 +1700,7 @@ function renderStalenessIndicator(label, cacheAge, showAge = true) {
|
|
|
1467
1700
|
}
|
|
1468
1701
|
const stalenessLevel = getStalenessLevel(cacheAge);
|
|
1469
1702
|
let text = label;
|
|
1470
|
-
if (showAge && cacheAge >=
|
|
1703
|
+
if (showAge && cacheAge >= STALENESS_THRESHOLD_MINUTES) {
|
|
1471
1704
|
text = `[stale ${cacheAge}m]`;
|
|
1472
1705
|
}
|
|
1473
1706
|
switch (stalenessLevel) {
|
|
@@ -1480,9 +1713,9 @@ function renderStalenessIndicator(label, cacheAge, showAge = true) {
|
|
|
1480
1713
|
}
|
|
1481
1714
|
}
|
|
1482
1715
|
function getStalenessLevel(ageMinutes) {
|
|
1483
|
-
if (ageMinutes <
|
|
1716
|
+
if (ageMinutes < STALENESS_THRESHOLD_MINUTES) {
|
|
1484
1717
|
return "fresh";
|
|
1485
|
-
} else if (ageMinutes <=
|
|
1718
|
+
} else if (ageMinutes <= VERY_STALE_THRESHOLD_MINUTES) {
|
|
1486
1719
|
return "stale";
|
|
1487
1720
|
} else {
|
|
1488
1721
|
return "very-stale";
|
|
@@ -1500,6 +1733,8 @@ function getDefaultMessage(errorState) {
|
|
|
1500
1733
|
return "authentication failed";
|
|
1501
1734
|
case "rate-limited":
|
|
1502
1735
|
return "rate limited";
|
|
1736
|
+
case "timeout":
|
|
1737
|
+
return "fetching...";
|
|
1503
1738
|
default:
|
|
1504
1739
|
return "error";
|
|
1505
1740
|
}
|
|
@@ -1665,56 +1900,117 @@ var PROGRESS_ICONS = [
|
|
|
1665
1900
|
"\uDB82\uDEA4",
|
|
1666
1901
|
"\uDB82\uDEA5"
|
|
1667
1902
|
];
|
|
1668
|
-
|
|
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
|
+
}
|
|
1669
1925
|
if (percent === null) {
|
|
1670
1926
|
return PROGRESS_ICONS[0] ?? "";
|
|
1671
1927
|
}
|
|
1672
1928
|
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
1673
|
-
const index =
|
|
1929
|
+
const index = calcNerdIconIndex(clampedPercent);
|
|
1674
1930
|
return PROGRESS_ICONS[index] ?? PROGRESS_ICONS[0] ?? "";
|
|
1675
1931
|
}
|
|
1676
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
|
+
|
|
1677
1972
|
// src/renderer/component.ts
|
|
1678
|
-
function renderComponent(componentId, data, componentConfig, globalConfig) {
|
|
1973
|
+
function renderComponent(componentId, data, componentConfig, globalConfig, renderContext) {
|
|
1679
1974
|
const effectiveLayout = componentConfig.layout ?? globalConfig.display.layout;
|
|
1680
|
-
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);
|
|
1681
1977
|
const effectiveBarSize = componentConfig.barSize ?? globalConfig.display.barSize;
|
|
1682
1978
|
const effectiveBarStyle = componentConfig.barStyle ?? globalConfig.display.barStyle;
|
|
1683
1979
|
const clockFormat = globalConfig.display.clockFormat;
|
|
1684
1980
|
switch (componentId) {
|
|
1685
1981
|
case "daily":
|
|
1686
|
-
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);
|
|
1687
1983
|
case "weekly":
|
|
1688
|
-
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);
|
|
1689
1985
|
case "monthly":
|
|
1690
|
-
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);
|
|
1691
1987
|
case "balance":
|
|
1692
|
-
return renderBalanceComponent(data.balance, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig);
|
|
1988
|
+
return renderBalanceComponent(data.balance, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, renderContext);
|
|
1693
1989
|
case "tokens":
|
|
1694
|
-
return renderTokensComponent(data.tokenStats, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig);
|
|
1990
|
+
return renderTokensComponent(data.tokenStats, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig, renderContext);
|
|
1695
1991
|
case "rateLimit":
|
|
1696
|
-
return renderRateLimitComponent(data.rateLimit, effectiveLayout, effectiveDisplayMode, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig);
|
|
1992
|
+
return renderRateLimitComponent(data.rateLimit, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, renderContext);
|
|
1697
1993
|
case "plan":
|
|
1698
|
-
return renderPlanComponent(data.planName, effectiveLayout, componentConfig, globalConfig);
|
|
1994
|
+
return renderPlanComponent(data.planName, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig, renderContext);
|
|
1699
1995
|
default:
|
|
1700
1996
|
return null;
|
|
1701
1997
|
}
|
|
1702
1998
|
}
|
|
1703
|
-
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) {
|
|
1704
2000
|
if (!quota)
|
|
1705
2001
|
return null;
|
|
1706
2002
|
const usagePercent = calculateUsagePercent(quota.used, quota.limit);
|
|
1707
|
-
const label = renderLabel(componentId,
|
|
2003
|
+
const label = renderLabel(componentId, displayMode, componentConfig, quota.qualifier);
|
|
1708
2004
|
const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
|
|
1709
2005
|
const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
|
|
1710
2006
|
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
1711
2007
|
const countdownColor = resolvePartColor("countdown", usagePercent, componentConfig, globalConfig);
|
|
1712
|
-
const
|
|
1713
|
-
const value = ansiColor(`${Math.round(usagePercent)}%`, valueColor);
|
|
1714
|
-
const countdown =
|
|
1715
|
-
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);
|
|
1716
2012
|
}
|
|
1717
|
-
function renderBalanceComponent(balance, layout, displayMode, barSize, barStyle, componentConfig, globalConfig) {
|
|
2013
|
+
function renderBalanceComponent(balance, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, renderContext) {
|
|
1718
2014
|
if (!balance)
|
|
1719
2015
|
return null;
|
|
1720
2016
|
const isUnlimited = balance.remaining === -1;
|
|
@@ -1723,120 +2019,171 @@ function renderBalanceComponent(balance, layout, displayMode, barSize, barStyle,
|
|
|
1723
2019
|
usagePercent = (balance.initial - balance.remaining) / balance.initial * 100;
|
|
1724
2020
|
}
|
|
1725
2021
|
const effectivePercent = isUnlimited ? 0 : usagePercent;
|
|
1726
|
-
const label = renderLabel("balance",
|
|
2022
|
+
const label = renderLabel("balance", displayMode, componentConfig);
|
|
1727
2023
|
const barColor = resolvePartColor("bar", effectivePercent, componentConfig, globalConfig);
|
|
1728
2024
|
const valueColor = resolvePartColor("value", effectivePercent, componentConfig, globalConfig);
|
|
1729
2025
|
const labelColor = resolvePartColor("label", effectivePercent, componentConfig, globalConfig);
|
|
1730
|
-
const
|
|
2026
|
+
const progress = isUnlimited ? "" : renderProgress(progressStyle, effectivePercent ?? 0, barSize, barStyle, barColor, null);
|
|
1731
2027
|
const valueText = isUnlimited ? "∞" : `$${balance.remaining.toFixed(2)}`;
|
|
1732
|
-
const value = ansiColor(valueText, valueColor);
|
|
2028
|
+
const value = ansiColor(valueText, valueColor, renderContext);
|
|
1733
2029
|
const countdown = "";
|
|
1734
|
-
return assembleComponent(layout, label, labelColor,
|
|
2030
|
+
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
1735
2031
|
}
|
|
1736
|
-
function renderTokensComponent(tokenStats, layout, displayMode, componentConfig, globalConfig) {
|
|
2032
|
+
function renderTokensComponent(tokenStats, layout, displayMode, componentConfig, globalConfig, renderContext) {
|
|
1737
2033
|
if (!tokenStats)
|
|
1738
2034
|
return null;
|
|
1739
2035
|
const stats = tokenStats.total ?? tokenStats.today;
|
|
1740
2036
|
if (!stats)
|
|
1741
2037
|
return null;
|
|
1742
|
-
const label = renderLabel("tokens",
|
|
2038
|
+
const label = renderLabel("tokens", displayMode, componentConfig);
|
|
1743
2039
|
const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
|
|
1744
2040
|
const valueColor = resolvePartColor("value", null, componentConfig, globalConfig);
|
|
1745
2041
|
const tokenCount = stats.totalTokens ?? stats.inputTokens + stats.outputTokens;
|
|
1746
|
-
const valueText =
|
|
1747
|
-
const value = ansiColor(valueText, valueColor);
|
|
1748
|
-
const
|
|
2042
|
+
const valueText = formatCompactNumber(tokenCount);
|
|
2043
|
+
const value = ansiColor(valueText, valueColor, renderContext);
|
|
2044
|
+
const progress = "";
|
|
1749
2045
|
const countdown = "";
|
|
1750
|
-
return assembleComponent(layout, label, labelColor,
|
|
2046
|
+
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
1751
2047
|
}
|
|
1752
|
-
function renderRateLimitComponent(rateLimit, layout, displayMode, barSize, barStyle, componentConfig, globalConfig) {
|
|
2048
|
+
function renderRateLimitComponent(rateLimit, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, renderContext) {
|
|
1753
2049
|
if (!rateLimit)
|
|
1754
2050
|
return null;
|
|
1755
2051
|
let usagePercent = null;
|
|
1756
2052
|
if (rateLimit.requestsLimit !== null && rateLimit.requestsLimit > 0) {
|
|
1757
2053
|
usagePercent = rateLimit.requestsUsed / rateLimit.requestsLimit * 100;
|
|
1758
2054
|
}
|
|
1759
|
-
const label = renderLabel("rateLimit",
|
|
2055
|
+
const label = renderLabel("rateLimit", displayMode, componentConfig);
|
|
1760
2056
|
const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
|
|
1761
2057
|
const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
|
|
1762
2058
|
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
1763
|
-
const
|
|
2059
|
+
const progress = usagePercent !== null ? renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, null) : "";
|
|
1764
2060
|
const valueText = rateLimit.requestsLimit !== null ? `${rateLimit.requestsUsed}/${rateLimit.requestsLimit}` : `${rateLimit.requestsUsed}`;
|
|
1765
|
-
const value = ansiColor(valueText, valueColor);
|
|
2061
|
+
const value = ansiColor(valueText, valueColor, renderContext);
|
|
1766
2062
|
const countdown = "";
|
|
1767
|
-
return assembleComponent(layout, label, labelColor,
|
|
2063
|
+
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
1768
2064
|
}
|
|
1769
|
-
function renderPlanComponent(planName, layout, componentConfig, globalConfig) {
|
|
1770
|
-
if (
|
|
2065
|
+
function renderPlanComponent(planName, layout, displayMode, componentConfig, globalConfig, renderContext) {
|
|
2066
|
+
if (displayMode === "hidden")
|
|
1771
2067
|
return null;
|
|
1772
2068
|
const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
|
|
1773
|
-
const value = ansiColor(planName, labelColor);
|
|
2069
|
+
const value = ansiColor(planName, labelColor, renderContext);
|
|
1774
2070
|
if (typeof componentConfig.label === "object" && componentConfig.label.text) {
|
|
1775
|
-
const labelText = ansiColor(componentConfig.label.text, labelColor);
|
|
2071
|
+
const labelText = ansiColor(componentConfig.label.text, labelColor, renderContext);
|
|
1776
2072
|
return `${labelText} ${value}`;
|
|
1777
2073
|
}
|
|
1778
2074
|
return value;
|
|
1779
2075
|
}
|
|
1780
|
-
function renderLabel(componentId,
|
|
1781
|
-
if (
|
|
2076
|
+
function renderLabel(componentId, displayMode, componentConfig, qualifier) {
|
|
2077
|
+
if (displayMode === "hidden")
|
|
1782
2078
|
return "";
|
|
1783
2079
|
if (componentConfig.label === false)
|
|
1784
2080
|
return "";
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
+
}
|
|
1793
2121
|
}
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
2122
|
+
if (qualifier) {
|
|
2123
|
+
if (displayMode === "compact") {
|
|
2124
|
+
return `${baseLabel}(${qualifier.charAt(0)})`;
|
|
2125
|
+
}
|
|
2126
|
+
return `${baseLabel}(${qualifier})`;
|
|
1796
2127
|
}
|
|
1797
|
-
return
|
|
2128
|
+
return baseLabel;
|
|
1798
2129
|
}
|
|
1799
|
-
function
|
|
1800
|
-
switch (
|
|
2130
|
+
function renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, emptyColor) {
|
|
2131
|
+
switch (progressStyle) {
|
|
1801
2132
|
case "bar":
|
|
1802
2133
|
return renderBar(usagePercent, barSize, barStyle, barColor, emptyColor);
|
|
1803
|
-
case "
|
|
2134
|
+
case "icon":
|
|
2135
|
+
return getProgressIcon(usagePercent, true);
|
|
2136
|
+
case "hidden":
|
|
1804
2137
|
return "";
|
|
1805
|
-
case "icon-pct":
|
|
1806
|
-
return getProgressIcon(usagePercent);
|
|
1807
2138
|
default:
|
|
1808
2139
|
return "";
|
|
1809
2140
|
}
|
|
1810
2141
|
}
|
|
1811
|
-
function
|
|
1812
|
-
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)
|
|
1813
2156
|
return "";
|
|
1814
2157
|
const config = typeof countdownConfig === "object" ? countdownConfig : {};
|
|
1815
|
-
const
|
|
1816
|
-
|
|
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 "";
|
|
1817
2170
|
}
|
|
1818
|
-
function assembleComponent(layout, label, labelColor,
|
|
1819
|
-
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;
|
|
1820
2173
|
const parts = [];
|
|
1821
|
-
if (layout === "
|
|
1822
|
-
if (display)
|
|
1823
|
-
parts.push(display);
|
|
1824
|
-
parts.push(value);
|
|
1825
|
-
if (countdown)
|
|
1826
|
-
parts.push(countdown);
|
|
1827
|
-
} else if (layout === "percent-first") {
|
|
2174
|
+
if (layout === "percent-first") {
|
|
1828
2175
|
if (coloredLabel)
|
|
1829
2176
|
parts.push(coloredLabel);
|
|
1830
2177
|
parts.push(value);
|
|
1831
|
-
if (
|
|
1832
|
-
parts.push(
|
|
2178
|
+
if (progress)
|
|
2179
|
+
parts.push(progress);
|
|
1833
2180
|
if (countdown)
|
|
1834
2181
|
parts.push(countdown);
|
|
1835
2182
|
} else {
|
|
1836
2183
|
if (coloredLabel)
|
|
1837
2184
|
parts.push(coloredLabel);
|
|
1838
|
-
if (
|
|
1839
|
-
parts.push(
|
|
2185
|
+
if (progress)
|
|
2186
|
+
parts.push(progress);
|
|
1840
2187
|
parts.push(value);
|
|
1841
2188
|
if (countdown)
|
|
1842
2189
|
parts.push(countdown);
|
|
@@ -1844,8 +2191,10 @@ function assembleComponent(layout, label, labelColor, display, value, countdown)
|
|
|
1844
2191
|
return parts.filter((p) => p).join(" ").replace(/ (\x1b\[[0-9;]*m)? ([·•])/g, "$1 $2");
|
|
1845
2192
|
}
|
|
1846
2193
|
function calculateUsagePercent(used, limit) {
|
|
1847
|
-
if (limit === null
|
|
2194
|
+
if (limit === null)
|
|
1848
2195
|
return 0;
|
|
2196
|
+
if (limit === 0)
|
|
2197
|
+
return 100;
|
|
1849
2198
|
return used / limit * 100;
|
|
1850
2199
|
}
|
|
1851
2200
|
function resolvePartColor(part, usagePercent, componentConfig, globalConfig) {
|
|
@@ -1856,16 +2205,6 @@ function resolvePartColor(part, usagePercent, componentConfig, globalConfig) {
|
|
|
1856
2205
|
const color = componentConfig.color ?? "auto";
|
|
1857
2206
|
return resolveColor(color, usagePercent, globalConfig);
|
|
1858
2207
|
}
|
|
1859
|
-
function formatLargeNumber(n) {
|
|
1860
|
-
if (n >= 1e9) {
|
|
1861
|
-
return `${(n / 1e9).toFixed(1)}B`;
|
|
1862
|
-
} else if (n >= 1e6) {
|
|
1863
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
1864
|
-
} else if (n >= 1000) {
|
|
1865
|
-
return `${(n / 1000).toFixed(1)}K`;
|
|
1866
|
-
}
|
|
1867
|
-
return n.toString();
|
|
1868
|
-
}
|
|
1869
2208
|
|
|
1870
2209
|
// src/renderer/truncate.ts
|
|
1871
2210
|
function getTerminalWidth() {
|
|
@@ -1931,17 +2270,75 @@ var COMPONENT_DROP_PRIORITY = [
|
|
|
1931
2270
|
"balance"
|
|
1932
2271
|
];
|
|
1933
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
|
+
|
|
1934
2339
|
// src/renderer/index.ts
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
"weekly",
|
|
1938
|
-
"monthly",
|
|
1939
|
-
"balance",
|
|
1940
|
-
"tokens",
|
|
1941
|
-
"rateLimit",
|
|
1942
|
-
"plan"
|
|
1943
|
-
];
|
|
1944
|
-
function renderStatusline(data, config, errorState, cacheAge) {
|
|
2340
|
+
function renderStatusline(data, config, errorState, cacheAge, isPiped = false) {
|
|
2341
|
+
const renderContext = createRenderContext(config, isPiped);
|
|
1945
2342
|
const componentOrder = getComponentOrder(config);
|
|
1946
2343
|
const componentMap = new Map;
|
|
1947
2344
|
for (const componentId of componentOrder) {
|
|
@@ -1949,26 +2346,21 @@ function renderStatusline(data, config, errorState, cacheAge) {
|
|
|
1949
2346
|
if (componentConfig === false) {
|
|
1950
2347
|
continue;
|
|
1951
2348
|
}
|
|
1952
|
-
const rendered = renderComponent(componentId, data, componentConfig === true || componentConfig === undefined ? {} : componentConfig, config);
|
|
2349
|
+
const rendered = renderComponent(componentId, data, componentConfig === true || componentConfig === undefined ? {} : componentConfig, config, renderContext);
|
|
1953
2350
|
if (rendered !== null) {
|
|
1954
2351
|
componentMap.set(componentId, rendered);
|
|
1955
2352
|
}
|
|
1956
2353
|
}
|
|
1957
|
-
const
|
|
1958
|
-
const maxWidth = computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
|
|
1959
|
-
const separator = config.display.separator ?? " | ";
|
|
2354
|
+
const separator = computeSeparator(config);
|
|
1960
2355
|
const activeComponents = new Set(componentMap.keys());
|
|
1961
2356
|
let currentWidth = calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge);
|
|
1962
2357
|
for (const dropCandidate of COMPONENT_DROP_PRIORITY) {
|
|
1963
|
-
if (dropCandidate === "countdown")
|
|
2358
|
+
if (dropCandidate === "countdown")
|
|
1964
2359
|
continue;
|
|
1965
|
-
|
|
1966
|
-
if (currentWidth <= maxWidth) {
|
|
2360
|
+
if (currentWidth <= maxWidth(config))
|
|
1967
2361
|
break;
|
|
1968
|
-
|
|
1969
|
-
if (activeComponents.size <= 1) {
|
|
2362
|
+
if (activeComponents.size <= 1)
|
|
1970
2363
|
break;
|
|
1971
|
-
}
|
|
1972
2364
|
if (activeComponents.has(dropCandidate)) {
|
|
1973
2365
|
activeComponents.delete(dropCandidate);
|
|
1974
2366
|
currentWidth = calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge);
|
|
@@ -1978,54 +2370,56 @@ function renderStatusline(data, config, errorState, cacheAge) {
|
|
|
1978
2370
|
for (const componentId of componentOrder) {
|
|
1979
2371
|
if (activeComponents.has(componentId)) {
|
|
1980
2372
|
const rendered = componentMap.get(componentId);
|
|
1981
|
-
if (rendered)
|
|
2373
|
+
if (rendered)
|
|
1982
2374
|
renderedComponents.push(rendered);
|
|
1983
|
-
}
|
|
1984
2375
|
}
|
|
1985
2376
|
}
|
|
1986
2377
|
let statusline = renderedComponents.join(separator);
|
|
1987
2378
|
if (errorState) {
|
|
1988
|
-
|
|
1989
|
-
if (isTransition) {
|
|
2379
|
+
if (isTransitionState(errorState)) {
|
|
1990
2380
|
statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
|
|
1991
2381
|
} else {
|
|
1992
2382
|
const hasCache = renderedComponents.length > 0;
|
|
1993
2383
|
const errorMode = hasCache ? "with-cache" : "without-cache";
|
|
1994
2384
|
const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
|
|
1995
|
-
|
|
1996
|
-
statusline = `${statusline} ${errorIndicator}`;
|
|
1997
|
-
} else {
|
|
1998
|
-
statusline = errorIndicator;
|
|
1999
|
-
}
|
|
2385
|
+
statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
|
|
2000
2386
|
}
|
|
2001
2387
|
}
|
|
2002
|
-
|
|
2388
|
+
const termWidth = getTerminalWidth();
|
|
2389
|
+
const maxW = computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
|
|
2390
|
+
statusline = ansiAwareTruncate(statusline, maxW);
|
|
2003
2391
|
return statusline;
|
|
2004
2392
|
}
|
|
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
|
+
}
|
|
2005
2405
|
function calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge) {
|
|
2006
2406
|
const components = [];
|
|
2007
2407
|
for (const id of componentOrder) {
|
|
2008
2408
|
if (activeComponents.has(id)) {
|
|
2009
2409
|
const rendered = componentMap.get(id);
|
|
2010
|
-
if (rendered)
|
|
2410
|
+
if (rendered)
|
|
2011
2411
|
components.push(rendered);
|
|
2012
|
-
}
|
|
2013
2412
|
}
|
|
2014
2413
|
}
|
|
2015
2414
|
let statusline = components.join(separator);
|
|
2016
2415
|
if (errorState) {
|
|
2017
|
-
|
|
2018
|
-
if (isTransition) {
|
|
2416
|
+
if (isTransitionState(errorState)) {
|
|
2019
2417
|
statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
|
|
2020
2418
|
} else {
|
|
2021
2419
|
const hasCache = components.length > 0;
|
|
2022
2420
|
const errorMode = hasCache ? "with-cache" : "without-cache";
|
|
2023
2421
|
const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
|
|
2024
|
-
|
|
2025
|
-
statusline = `${statusline} ${errorIndicator}`;
|
|
2026
|
-
} else {
|
|
2027
|
-
statusline = errorIndicator;
|
|
2028
|
-
}
|
|
2422
|
+
statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
|
|
2029
2423
|
}
|
|
2030
2424
|
}
|
|
2031
2425
|
return visibleLength(statusline);
|
|
@@ -2040,7 +2434,7 @@ function getComponentOrder(config) {
|
|
|
2040
2434
|
}
|
|
2041
2435
|
}
|
|
2042
2436
|
const order = [...explicitOrder];
|
|
2043
|
-
for (const componentId of
|
|
2437
|
+
for (const componentId of DEFAULT_COMPONENT_ORDER) {
|
|
2044
2438
|
if (!explicitSet.has(componentId)) {
|
|
2045
2439
|
order.push(componentId);
|
|
2046
2440
|
}
|
|
@@ -2048,7 +2442,7 @@ function getComponentOrder(config) {
|
|
|
2048
2442
|
return order;
|
|
2049
2443
|
}
|
|
2050
2444
|
function isComponentId(key) {
|
|
2051
|
-
return
|
|
2445
|
+
return DEFAULT_COMPONENT_ORDER.includes(key);
|
|
2052
2446
|
}
|
|
2053
2447
|
|
|
2054
2448
|
// src/core/execute-cycle.ts
|
|
@@ -2082,23 +2476,16 @@ async function executeCycle(ctx) {
|
|
|
2082
2476
|
cacheUpdate: updatedEntry
|
|
2083
2477
|
};
|
|
2084
2478
|
}
|
|
2085
|
-
const deadline = startTime + timeoutBudgetMs -
|
|
2479
|
+
const deadline = startTime + timeoutBudgetMs - EXIT_BUFFER_MS;
|
|
2086
2480
|
const remainingBudget = deadline - Date.now();
|
|
2087
2481
|
if (remainingBudget <= 50) {
|
|
2088
|
-
logger.debug("Path
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
} else {
|
|
2096
|
-
return {
|
|
2097
|
-
output: "[loading...]",
|
|
2098
|
-
exitCode: 0,
|
|
2099
|
-
cacheUpdate: null
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
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
|
+
};
|
|
2102
2489
|
}
|
|
2103
2490
|
try {
|
|
2104
2491
|
const baseUrl = env.baseUrl;
|
|
@@ -2136,225 +2523,221 @@ async function executeCycle(ctx) {
|
|
|
2136
2523
|
};
|
|
2137
2524
|
} catch (error) {
|
|
2138
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
|
+
}
|
|
2139
2537
|
if (cachedEntry) {
|
|
2140
|
-
|
|
2141
|
-
const statusline = renderStatusline(cachedEntry.data, config, "network-error", ageMinutes);
|
|
2142
|
-
logger.debug("Using stale cache with error indicator", { ageMinutes });
|
|
2143
|
-
return {
|
|
2144
|
-
output: statusline,
|
|
2145
|
-
exitCode: 0,
|
|
2146
|
-
cacheUpdate: null
|
|
2147
|
-
};
|
|
2538
|
+
logger.debug("Discarding stale cache, showing error", { errorState });
|
|
2148
2539
|
} else {
|
|
2149
2540
|
logger.warn("No cache available for error fallback");
|
|
2150
|
-
const errorOutput = renderError("network-error", "without-cache", providerId);
|
|
2151
|
-
return {
|
|
2152
|
-
output: errorOutput,
|
|
2153
|
-
exitCode: 0,
|
|
2154
|
-
cacheUpdate: null
|
|
2155
|
-
};
|
|
2156
2541
|
}
|
|
2542
|
+
const errorOutput = renderError(errorState, "without-cache", providerId);
|
|
2543
|
+
return {
|
|
2544
|
+
output: errorOutput,
|
|
2545
|
+
exitCode: 0,
|
|
2546
|
+
cacheUpdate: null
|
|
2547
|
+
};
|
|
2157
2548
|
}
|
|
2158
2549
|
}
|
|
2159
|
-
// src/services/
|
|
2160
|
-
import {
|
|
2161
|
-
import {
|
|
2162
|
-
|
|
2163
|
-
function loadClaudeSettings() {
|
|
2164
|
-
const path = getSettingsJsonPath();
|
|
2165
|
-
if (!existsSync5(path)) {
|
|
2166
|
-
return {};
|
|
2167
|
-
}
|
|
2168
|
-
try {
|
|
2169
|
-
const content = readFileSync4(path, "utf-8");
|
|
2170
|
-
return JSON.parse(content);
|
|
2171
|
-
} catch (error) {
|
|
2172
|
-
console.warn(`Failed to read settings from ${path}: ${error}`);
|
|
2173
|
-
return {};
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
function saveClaudeSettings(settings) {
|
|
2177
|
-
const path = getSettingsJsonPath();
|
|
2178
|
-
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) {
|
|
2179
2554
|
try {
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2555
|
+
if (!existsSync7(cacheDir)) {
|
|
2556
|
+
logger.debug("GC: Cache directory does not exist, skipping", { cacheDir });
|
|
2557
|
+
return;
|
|
2183
2558
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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) });
|
|
2193
2578
|
}
|
|
2194
|
-
}
|
|
2195
|
-
|
|
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) });
|
|
2196
2638
|
}
|
|
2197
2639
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
+
})();
|
|
2201
2696
|
}
|
|
2202
|
-
function
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
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...]";
|
|
2208
2702
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
padding: 0
|
|
2216
|
-
};
|
|
2217
|
-
saveClaudeSettings(settings);
|
|
2218
|
-
}
|
|
2219
|
-
function uninstallStatusLine() {
|
|
2220
|
-
const settings = loadClaudeSettings();
|
|
2221
|
-
if ("statusLine" in settings) {
|
|
2222
|
-
delete settings.statusLine;
|
|
2223
|
-
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;
|
|
2224
2709
|
}
|
|
2225
2710
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
"statusline",
|
|
2252
|
-
"api",
|
|
2253
|
-
"usage",
|
|
2254
|
-
"monitoring",
|
|
2255
|
-
"tui",
|
|
2256
|
-
"cli"
|
|
2257
|
-
],
|
|
2258
|
-
author: "Liafonx",
|
|
2259
|
-
license: "MIT",
|
|
2260
|
-
repository: {
|
|
2261
|
-
type: "git",
|
|
2262
|
-
url: "git+https://github.com/liafonx/cc-api-statusline.git"
|
|
2263
|
-
},
|
|
2264
|
-
homepage: "https://github.com/liafonx/cc-api-statusline#readme",
|
|
2265
|
-
bugs: {
|
|
2266
|
-
url: "https://github.com/liafonx/cc-api-statusline/issues"
|
|
2267
|
-
},
|
|
2268
|
-
dependencies: {},
|
|
2269
|
-
devDependencies: {
|
|
2270
|
-
"@eslint/js": "^9.17.0",
|
|
2271
|
-
"@types/bun": "^1.1.14",
|
|
2272
|
-
eslint: "^9.17.0",
|
|
2273
|
-
typescript: "^5.7.2",
|
|
2274
|
-
"typescript-eslint": "^8.18.2",
|
|
2275
|
-
vitest: "^2.1.8"
|
|
2276
|
-
},
|
|
2277
|
-
engines: {
|
|
2278
|
-
node: ">=18.0.0"
|
|
2279
|
-
},
|
|
2280
|
-
publishConfig: {
|
|
2281
|
-
provenance: true
|
|
2282
|
-
}
|
|
2283
|
-
};
|
|
2284
|
-
|
|
2285
|
-
// src/main.ts
|
|
2286
|
-
function parseArgs() {
|
|
2287
|
-
const args = process.argv.slice(2);
|
|
2288
|
-
let help = false;
|
|
2289
|
-
let version = false;
|
|
2290
|
-
let once = false;
|
|
2291
|
-
let install = false;
|
|
2292
|
-
let uninstall = false;
|
|
2293
|
-
let force = false;
|
|
2294
|
-
let configPath;
|
|
2295
|
-
let runner;
|
|
2296
|
-
for (let i = 0;i < args.length; i++) {
|
|
2297
|
-
const arg = args[i];
|
|
2298
|
-
if (arg === "--help" || arg === "-h") {
|
|
2299
|
-
help = true;
|
|
2300
|
-
} else if (arg === "--version" || arg === "-v") {
|
|
2301
|
-
version = true;
|
|
2302
|
-
} else if (arg === "--once") {
|
|
2303
|
-
once = true;
|
|
2304
|
-
} else if (arg === "--install") {
|
|
2305
|
-
install = true;
|
|
2306
|
-
} else if (arg === "--uninstall") {
|
|
2307
|
-
uninstall = true;
|
|
2308
|
-
} else if (arg === "--force") {
|
|
2309
|
-
force = true;
|
|
2310
|
-
} else if (arg === "--config" && i + 1 < args.length) {
|
|
2311
|
-
configPath = args[i + 1];
|
|
2312
|
-
i++;
|
|
2313
|
-
} else if (arg === "--runner" && i + 1 < args.length) {
|
|
2314
|
-
const nextArg = args[i + 1];
|
|
2315
|
-
if (nextArg === "npx" || nextArg === "bunx") {
|
|
2316
|
-
runner = nextArg;
|
|
2317
|
-
}
|
|
2318
|
-
i++;
|
|
2319
|
-
}
|
|
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());
|
|
2320
2736
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
function showHelp() {
|
|
2324
|
-
console.log(`
|
|
2325
|
-
cc-api-statusline — Claude API statusline widget
|
|
2326
|
-
|
|
2327
|
-
Usage:
|
|
2328
|
-
cc-api-statusline [options]
|
|
2329
|
-
|
|
2330
|
-
Options:
|
|
2331
|
-
--help, -h Show this help message
|
|
2332
|
-
--version, -v Show version
|
|
2333
|
-
--once Fetch once and exit (no polling)
|
|
2334
|
-
--config <path> Use custom config file
|
|
2335
|
-
--install Register as Claude Code statusline widget
|
|
2336
|
-
--uninstall Remove statusline widget registration
|
|
2337
|
-
--runner <runner> Package runner: npx or bunx (default: auto-detect)
|
|
2338
|
-
--force Force overwrite existing statusline configuration
|
|
2339
|
-
|
|
2340
|
-
Environment Variables:
|
|
2341
|
-
ANTHROPIC_BASE_URL API endpoint (required)
|
|
2342
|
-
ANTHROPIC_AUTH_TOKEN API key (required)
|
|
2343
|
-
CC_STATUSLINE_PROVIDER Override provider detection
|
|
2344
|
-
CC_STATUSLINE_POLL Override poll interval (seconds)
|
|
2345
|
-
CC_STATUSLINE_TIMEOUT Piped mode timeout (milliseconds, default 1000)
|
|
2346
|
-
DEBUG or CC_STATUSLINE_DEBUG Enable debug logging to ~/.claude/cc-api-statusline/debug.log
|
|
2347
|
-
|
|
2348
|
-
Config File:
|
|
2349
|
-
~/.claude/cc-api-statusline/config.json
|
|
2350
|
-
|
|
2351
|
-
Documentation:
|
|
2352
|
-
https://github.com/liafonx/cc-api-statusline
|
|
2353
|
-
`.trim());
|
|
2354
|
-
}
|
|
2355
|
-
function showVersion() {
|
|
2356
|
-
console.log(`cc-api-statusline v${package_default.version}`);
|
|
2737
|
+
logger.debug("=== cc-api-statusline execution completed ===");
|
|
2738
|
+
process.exit(result.exitCode);
|
|
2357
2739
|
}
|
|
2740
|
+
// src/main.ts
|
|
2358
2741
|
function discardStdin() {
|
|
2359
2742
|
if (!process.stdin.isTTY) {
|
|
2360
2743
|
process.stdin.resume();
|
|
@@ -2362,9 +2745,7 @@ function discardStdin() {
|
|
|
2362
2745
|
}
|
|
2363
2746
|
}
|
|
2364
2747
|
async function main() {
|
|
2365
|
-
|
|
2366
|
-
logger.debug("=== cc-api-statusline execution started ===");
|
|
2367
|
-
logger.debug("Start time", { startTime, version: package_default.version });
|
|
2748
|
+
logger.debug("=== cc-api-statusline execution started ===", { version: package_default.version });
|
|
2368
2749
|
discardStdin();
|
|
2369
2750
|
const args = parseArgs();
|
|
2370
2751
|
logger.debug("Parsed arguments", { args });
|
|
@@ -2377,119 +2758,22 @@ async function main() {
|
|
|
2377
2758
|
process.exit(0);
|
|
2378
2759
|
}
|
|
2379
2760
|
if (args.install) {
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
console.error("Error: statusLine is already configured in settings.json");
|
|
2383
|
-
console.error(`Current command: ${existing}`);
|
|
2384
|
-
console.error("Use --force to overwrite, or --uninstall to remove first.");
|
|
2385
|
-
process.exit(1);
|
|
2386
|
-
}
|
|
2387
|
-
const runner = args.runner ?? (isBunxAvailable() ? "bunx" : "npx");
|
|
2388
|
-
installStatusLine(runner);
|
|
2389
|
-
console.log("✓ Statusline installed successfully!");
|
|
2390
|
-
console.log(` Runner: ${runner}`);
|
|
2391
|
-
console.log(` Command: ${runner} -y cc-api-statusline@latest`);
|
|
2392
|
-
console.log(` Config: ~/.claude/settings.json`);
|
|
2393
|
-
process.exit(0);
|
|
2761
|
+
handleInstall(args);
|
|
2762
|
+
return;
|
|
2394
2763
|
}
|
|
2395
2764
|
if (args.uninstall) {
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
console.log("No statusLine configuration found in settings.json");
|
|
2399
|
-
process.exit(0);
|
|
2400
|
-
}
|
|
2401
|
-
uninstallStatusLine();
|
|
2402
|
-
console.log("✓ Statusline uninstalled successfully");
|
|
2403
|
-
console.log(" Removed statusLine from ~/.claude/settings.json");
|
|
2404
|
-
process.exit(0);
|
|
2765
|
+
handleUninstall();
|
|
2766
|
+
return;
|
|
2405
2767
|
}
|
|
2406
|
-
|
|
2407
|
-
logger.debug("Mode detection", { isPiped, once: args.once });
|
|
2408
|
-
if (!isPiped && !args.once) {
|
|
2768
|
+
if (process.stdin.isTTY && !args.once) {
|
|
2409
2769
|
console.log("Interactive configuration mode coming soon.");
|
|
2410
2770
|
console.log("Use --once for a single fetch, or configure as a Claude Code statusline command.");
|
|
2411
2771
|
process.exit(0);
|
|
2412
2772
|
}
|
|
2413
|
-
|
|
2414
|
-
logger.debug("Environment loaded", {
|
|
2415
|
-
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
2416
|
-
hasToken: !!env.authToken,
|
|
2417
|
-
providerOverride: env.providerOverride,
|
|
2418
|
-
pollIntervalOverride: env.pollIntervalOverride
|
|
2419
|
-
});
|
|
2420
|
-
const envError = validateRequiredEnv(env);
|
|
2421
|
-
if (envError) {
|
|
2422
|
-
const errorOutput = renderError("missing-env", "without-cache");
|
|
2423
|
-
process.stdout.write(errorOutput);
|
|
2424
|
-
process.exit(0);
|
|
2425
|
-
}
|
|
2426
|
-
const baseUrl = env.baseUrl;
|
|
2427
|
-
const authToken = env.authToken;
|
|
2428
|
-
if (!baseUrl || !authToken) {
|
|
2429
|
-
process.exit(1);
|
|
2430
|
-
}
|
|
2431
|
-
const config = loadConfig(args.configPath);
|
|
2432
|
-
const configPath = getConfigPath(args.configPath);
|
|
2433
|
-
const configHash = computeConfigHash(configPath);
|
|
2434
|
-
logger.debug("Config loaded", { configPath, configHash });
|
|
2435
|
-
const probeTimeout = isPiped ? Math.min(1500, Math.max(200, Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) - 200)) : 3000;
|
|
2436
|
-
const providerId = await resolveProvider(baseUrl, env.providerOverride, config.customProviders ?? {}, probeTimeout);
|
|
2437
|
-
const provider = getProvider(providerId, config.customProviders ?? {});
|
|
2438
|
-
logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
2439
|
-
if (!provider) {
|
|
2440
|
-
logger.error("Provider not found", { providerId });
|
|
2441
|
-
const errorOutput = renderError("provider-unknown", "without-cache");
|
|
2442
|
-
process.stdout.write(errorOutput);
|
|
2443
|
-
process.exit(0);
|
|
2444
|
-
}
|
|
2445
|
-
const cachedEntry = readCache(baseUrl);
|
|
2446
|
-
logger.debug("Cache read", {
|
|
2447
|
-
cacheHit: !!cachedEntry,
|
|
2448
|
-
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
2449
|
-
});
|
|
2450
|
-
const timeoutBudgetMs = isPiped ? Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) : 1e4;
|
|
2451
|
-
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? 800, timeoutBudgetMs - 100) : 1e4;
|
|
2452
|
-
const ctx = {
|
|
2453
|
-
env,
|
|
2454
|
-
config,
|
|
2455
|
-
configHash,
|
|
2456
|
-
cachedEntry,
|
|
2457
|
-
providerId,
|
|
2458
|
-
provider,
|
|
2459
|
-
timeoutBudgetMs,
|
|
2460
|
-
startTime,
|
|
2461
|
-
fetchTimeoutMs
|
|
2462
|
-
};
|
|
2463
|
-
logger.debug("Execution context prepared", { timeoutBudgetMs, fetchTimeoutMs });
|
|
2464
|
-
const result = await executeCycle(ctx);
|
|
2465
|
-
const executionTime = Date.now() - startTime;
|
|
2466
|
-
logger.debug("Execution completed", {
|
|
2467
|
-
exitCode: result.exitCode,
|
|
2468
|
-
executionTime: `${executionTime}ms`,
|
|
2469
|
-
outputLength: result.output.length,
|
|
2470
|
-
cacheUpdate: !!result.cacheUpdate
|
|
2471
|
-
});
|
|
2472
|
-
let output = result.output;
|
|
2473
|
-
if (!output || output.trim().length === 0) {
|
|
2474
|
-
output = "[loading...]";
|
|
2475
|
-
logger.debug("Empty output detected, using fallback");
|
|
2476
|
-
}
|
|
2477
|
-
if (isPiped) {
|
|
2478
|
-
const formatted = "\x1B[0m" + output.replace(/ /g, " ");
|
|
2479
|
-
process.stdout.write(formatted);
|
|
2480
|
-
logger.debug("Output formatted for piped mode (ANSI reset + NBSP)");
|
|
2481
|
-
} else {
|
|
2482
|
-
process.stdout.write(output);
|
|
2483
|
-
logger.debug("Output written (TTY mode)");
|
|
2484
|
-
}
|
|
2485
|
-
if (result.cacheUpdate) {
|
|
2486
|
-
writeCache(baseUrl, result.cacheUpdate);
|
|
2487
|
-
logger.debug("Cache updated");
|
|
2488
|
-
}
|
|
2489
|
-
logger.debug("=== Execution finished ===", { exitCode: result.exitCode });
|
|
2490
|
-
process.exit(result.exitCode);
|
|
2773
|
+
await executePipedMode(args);
|
|
2491
2774
|
}
|
|
2492
2775
|
main().catch((error) => {
|
|
2493
|
-
|
|
2776
|
+
logger.error("Unhandled error in main", { error: String(error) });
|
|
2777
|
+
console.error(`Fatal error: ${error}`);
|
|
2494
2778
|
process.exit(1);
|
|
2495
2779
|
});
|