cc-api-statusline 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cc-api-statusline.js +1158 -538
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
4
4
|
// package.json
|
|
5
5
|
var package_default = {
|
|
6
6
|
name: "cc-api-statusline",
|
|
7
|
-
version: "0.2.
|
|
7
|
+
version: "0.2.2",
|
|
8
8
|
description: "Claude Code statusline tool that polls API usage from third-party proxy backends",
|
|
9
9
|
type: "module",
|
|
10
10
|
bin: {
|
|
@@ -68,6 +68,7 @@ function parseArgs() {
|
|
|
68
68
|
let once = false;
|
|
69
69
|
let install = false;
|
|
70
70
|
let uninstall = false;
|
|
71
|
+
let applyConfig = false;
|
|
71
72
|
let force = false;
|
|
72
73
|
let configPath;
|
|
73
74
|
let runner;
|
|
@@ -83,6 +84,8 @@ function parseArgs() {
|
|
|
83
84
|
install = true;
|
|
84
85
|
} else if (arg === "--uninstall") {
|
|
85
86
|
uninstall = true;
|
|
87
|
+
} else if (arg === "--apply-config") {
|
|
88
|
+
applyConfig = true;
|
|
86
89
|
} else if (arg === "--force") {
|
|
87
90
|
force = true;
|
|
88
91
|
} else if (arg === "--config" && i + 1 < args.length) {
|
|
@@ -96,7 +99,7 @@ function parseArgs() {
|
|
|
96
99
|
i++;
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
|
-
return { help, version, once, install, uninstall, force, configPath, runner };
|
|
102
|
+
return { help, version, once, install, uninstall, applyConfig, force, configPath, runner };
|
|
100
103
|
}
|
|
101
104
|
function showHelp() {
|
|
102
105
|
console.log(`
|
|
@@ -110,8 +113,9 @@ Options:
|
|
|
110
113
|
--version, -v Show version
|
|
111
114
|
--once Fetch once and exit (no polling)
|
|
112
115
|
--config <path> Use custom config file
|
|
113
|
-
--install Register as Claude Code statusline widget
|
|
116
|
+
--install Register as Claude Code statusline widget and create default configs
|
|
114
117
|
--uninstall Remove statusline widget registration
|
|
118
|
+
--apply-config Apply endpoint config changes (updates lock file, clears caches)
|
|
115
119
|
--runner <runner> Package runner: npx or bunx (default: auto-detect)
|
|
116
120
|
--force Force overwrite existing statusline configuration
|
|
117
121
|
|
|
@@ -321,38 +325,10 @@ function uninstallStatusLine() {
|
|
|
321
325
|
}
|
|
322
326
|
}
|
|
323
327
|
|
|
324
|
-
// src/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
}
|
|
352
|
-
// src/services/cache.ts
|
|
353
|
-
import { readFileSync as readFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
354
|
-
import { join as join2 } from "path";
|
|
355
|
-
import { homedir as homedir2 } from "os";
|
|
328
|
+
// src/services/config-defaults.ts
|
|
329
|
+
import { join as join6 } from "path";
|
|
330
|
+
import { homedir as homedir5 } from "os";
|
|
331
|
+
import { existsSync as existsSync7 } from "fs";
|
|
356
332
|
|
|
357
333
|
// src/types/normalized-usage.ts
|
|
358
334
|
function createEmptyNormalizedUsage(provider, billingMode, planName) {
|
|
@@ -411,22 +387,68 @@ var DEFAULT_CONFIG = {
|
|
|
411
387
|
colors: {
|
|
412
388
|
auto: {
|
|
413
389
|
tiers: [
|
|
414
|
-
{ color: "cool", maxPercent:
|
|
415
|
-
{ color: "comfortable", maxPercent:
|
|
416
|
-
{ color: "warm", maxPercent:
|
|
417
|
-
{ color: "hot", maxPercent:
|
|
418
|
-
{ color: "critical", maxPercent:
|
|
390
|
+
{ color: "cool", maxPercent: 37.5 },
|
|
391
|
+
{ color: "comfortable", maxPercent: 62.5 },
|
|
392
|
+
{ color: "warm", maxPercent: 75 },
|
|
393
|
+
{ color: "hot", maxPercent: 85 },
|
|
394
|
+
{ color: "critical", maxPercent: 92.5 }
|
|
395
|
+
]
|
|
396
|
+
},
|
|
397
|
+
vibrant: {
|
|
398
|
+
tiers: [
|
|
399
|
+
{ color: "#00D9FF", maxPercent: 37.5 },
|
|
400
|
+
{ color: "#4ADE80", maxPercent: 62.5 },
|
|
401
|
+
{ color: "#FDE047", maxPercent: 75 },
|
|
402
|
+
{ color: "#FB923C", maxPercent: 85 },
|
|
403
|
+
{ color: "#F87171", maxPercent: 92.5 }
|
|
404
|
+
]
|
|
405
|
+
},
|
|
406
|
+
pastel: {
|
|
407
|
+
tiers: [
|
|
408
|
+
{ color: "pastel-cool", maxPercent: 37.5 },
|
|
409
|
+
{ color: "pastel-comfortable", maxPercent: 62.5 },
|
|
410
|
+
{ color: "pastel-medium", maxPercent: 75 },
|
|
411
|
+
{ color: "pastel-warm", maxPercent: 85 },
|
|
412
|
+
{ color: "pastel-hot", maxPercent: 92.5 }
|
|
413
|
+
]
|
|
414
|
+
},
|
|
415
|
+
bright: {
|
|
416
|
+
tiers: [
|
|
417
|
+
{ color: "bright-cool", maxPercent: 37.5 },
|
|
418
|
+
{ color: "bright-comfortable", maxPercent: 62.5 },
|
|
419
|
+
{ color: "bright-medium", maxPercent: 75 },
|
|
420
|
+
{ color: "bright-warm", maxPercent: 85 },
|
|
421
|
+
{ color: "bright-hot", maxPercent: 92.5 }
|
|
422
|
+
]
|
|
423
|
+
},
|
|
424
|
+
ocean: {
|
|
425
|
+
tiers: [
|
|
426
|
+
{ color: "ocean-cool", maxPercent: 37.5 },
|
|
427
|
+
{ color: "ocean-comfortable", maxPercent: 62.5 },
|
|
428
|
+
{ color: "ocean-medium", maxPercent: 75 },
|
|
429
|
+
{ color: "ocean-warm", maxPercent: 85 },
|
|
430
|
+
{ color: "ocean-hot", maxPercent: 92.5 }
|
|
431
|
+
]
|
|
432
|
+
},
|
|
433
|
+
neutral: {
|
|
434
|
+
tiers: [
|
|
435
|
+
{ color: "neutral-cool", maxPercent: 37.5 },
|
|
436
|
+
{ color: "neutral-comfortable", maxPercent: 62.5 },
|
|
437
|
+
{ color: "neutral-warm", maxPercent: 75 },
|
|
438
|
+
{ color: "neutral-hot", maxPercent: 85 },
|
|
439
|
+
{ color: "neutral-critical", maxPercent: 92.5 }
|
|
419
440
|
]
|
|
420
441
|
},
|
|
421
442
|
chill: {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
443
|
+
tiers: [
|
|
444
|
+
{ color: "cyan", maxPercent: 37.5 },
|
|
445
|
+
{ color: "cyan", maxPercent: 62.5 },
|
|
446
|
+
{ color: "blue", maxPercent: 75 },
|
|
447
|
+
{ color: "blue", maxPercent: 87.5 },
|
|
448
|
+
{ color: "magenta", maxPercent: 92.5 }
|
|
449
|
+
]
|
|
427
450
|
}
|
|
428
451
|
},
|
|
429
|
-
customProviders: {},
|
|
430
452
|
pollIntervalSeconds: 30,
|
|
431
453
|
pipedRequestTimeoutMs: 800
|
|
432
454
|
};
|
|
@@ -493,12 +515,12 @@ var DEFAULT_COMPONENT_ORDER = [
|
|
|
493
515
|
"plan"
|
|
494
516
|
];
|
|
495
517
|
// src/types/cache.ts
|
|
496
|
-
var CACHE_VERSION =
|
|
518
|
+
var CACHE_VERSION = 2;
|
|
497
519
|
function isCacheEntry(value) {
|
|
498
520
|
if (typeof value !== "object" || value === null)
|
|
499
521
|
return false;
|
|
500
522
|
const c = value;
|
|
501
|
-
return typeof c["version"] === "number" && typeof c["provider"] === "string" && typeof c["baseUrl"] === "string" && typeof c["tokenHash"] === "string" && typeof c["configHash"] === "string" && typeof c["data"] === "object" && c["data"] !== null && typeof c["renderedLine"] === "string" && typeof c["fetchedAt"] === "string" && typeof c["ttlSeconds"] === "number" && (c["errorState"] === null || typeof c["errorState"] === "object");
|
|
523
|
+
return typeof c["version"] === "number" && typeof c["provider"] === "string" && typeof c["baseUrl"] === "string" && typeof c["tokenHash"] === "string" && typeof c["configHash"] === "string" && typeof c["endpointConfigHash"] === "string" && typeof c["data"] === "object" && c["data"] !== null && typeof c["renderedLine"] === "string" && typeof c["fetchedAt"] === "string" && typeof c["ttlSeconds"] === "number" && (c["errorState"] === null || typeof c["errorState"] === "object");
|
|
502
524
|
}
|
|
503
525
|
var PROVIDER_DETECTION_TTL_SECONDS = 86400;
|
|
504
526
|
function isProviderDetectionCacheEntry(value) {
|
|
@@ -507,209 +529,458 @@ function isProviderDetectionCacheEntry(value) {
|
|
|
507
529
|
const c = value;
|
|
508
530
|
return typeof c["baseUrl"] === "string" && typeof c["provider"] === "string" && (c["detectedVia"] === "health-probe" || c["detectedVia"] === "url-pattern" || c["detectedVia"] === "override") && typeof c["detectedAt"] === "string" && typeof c["ttlSeconds"] === "number";
|
|
509
531
|
}
|
|
510
|
-
// src/services/
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
532
|
+
// src/services/endpoint-config.ts
|
|
533
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
534
|
+
import { join as join4 } from "path";
|
|
535
|
+
import { homedir as homedir3 } from "os";
|
|
536
|
+
|
|
537
|
+
// src/services/logger.ts
|
|
538
|
+
import { appendFileSync } from "fs";
|
|
539
|
+
import { join as join3, dirname as dirname3 } from "path";
|
|
540
|
+
import { homedir as homedir2 } from "os";
|
|
541
|
+
|
|
542
|
+
// src/services/log-rotator.ts
|
|
543
|
+
import { statSync, renameSync as renameSync2, readdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
544
|
+
import { spawn } from "child_process";
|
|
545
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
546
|
+
|
|
547
|
+
// src/core/constants.ts
|
|
548
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 5000;
|
|
549
|
+
var EXIT_BUFFER_MS = 50;
|
|
550
|
+
var STALENESS_THRESHOLD_MINUTES = 5;
|
|
551
|
+
var VERY_STALE_THRESHOLD_MINUTES = 30;
|
|
552
|
+
var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
553
|
+
var GC_MAX_CACHE_FILES = 20;
|
|
554
|
+
var GC_ORPHAN_TMP_AGE_MS = 60 * 60 * 1000;
|
|
555
|
+
var LOG_ROTATION_PROBABILITY = 0.05;
|
|
556
|
+
var LOG_MAX_SIZE_BYTES = 512 * 1024;
|
|
557
|
+
var LOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
558
|
+
var LOG_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
|
|
559
|
+
|
|
560
|
+
// src/services/log-rotator.ts
|
|
561
|
+
var ARCHIVE_LOG_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log$/;
|
|
562
|
+
var ARCHIVE_GZ_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log\.gz$/;
|
|
563
|
+
function archiveName(logPath, now = new Date) {
|
|
564
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
565
|
+
const y = now.getFullYear();
|
|
566
|
+
const mo = pad(now.getMonth() + 1);
|
|
567
|
+
const d = pad(now.getDate());
|
|
568
|
+
const h = pad(now.getHours());
|
|
569
|
+
const min = pad(now.getMinutes());
|
|
570
|
+
return join2(dirname2(logPath), `debug.${y}-${mo}-${d}T${h}-${min}.log`);
|
|
571
|
+
}
|
|
572
|
+
function spawnGzip(filePath) {
|
|
573
|
+
try {
|
|
574
|
+
const child = spawn("gzip", ["-f", filePath], {
|
|
575
|
+
detached: true,
|
|
576
|
+
stdio: "ignore"
|
|
577
|
+
});
|
|
578
|
+
child.unref();
|
|
579
|
+
} catch {}
|
|
517
580
|
}
|
|
518
|
-
function
|
|
519
|
-
|
|
520
|
-
|
|
581
|
+
function runCleanup(logDir, excludePath) {
|
|
582
|
+
try {
|
|
583
|
+
const files = readdirSync(logDir);
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
for (const name of files) {
|
|
586
|
+
const filePath = join2(logDir, name);
|
|
587
|
+
if (filePath === excludePath)
|
|
588
|
+
continue;
|
|
589
|
+
if (ARCHIVE_LOG_RE.test(name)) {
|
|
590
|
+
const s = statSync(filePath, { throwIfNoEntry: false });
|
|
591
|
+
if (s && now - s.mtimeMs >= LOG_MAX_AGE_MS) {
|
|
592
|
+
spawnGzip(filePath);
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (ARCHIVE_GZ_RE.test(name)) {
|
|
597
|
+
const s = statSync(filePath, { throwIfNoEntry: false });
|
|
598
|
+
if (s && now - s.mtimeMs >= LOG_RETENTION_MS) {
|
|
599
|
+
try {
|
|
600
|
+
unlinkSync2(filePath);
|
|
601
|
+
} catch {}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch {}
|
|
521
606
|
}
|
|
522
|
-
function
|
|
523
|
-
|
|
524
|
-
|
|
607
|
+
function maybeRotateLogs(logPath) {
|
|
608
|
+
if (Math.random() > LOG_ROTATION_PROBABILITY)
|
|
609
|
+
return;
|
|
610
|
+
const logDir = dirname2(logPath);
|
|
611
|
+
const stat = statSync(logPath, { throwIfNoEntry: false });
|
|
612
|
+
let rotatedArchive = null;
|
|
613
|
+
if (stat) {
|
|
614
|
+
const age = Date.now() - stat.mtimeMs;
|
|
615
|
+
const archive = archiveName(logPath);
|
|
616
|
+
try {
|
|
617
|
+
if (age >= LOG_MAX_AGE_MS) {
|
|
618
|
+
renameSync2(logPath, archive);
|
|
619
|
+
spawnGzip(archive);
|
|
620
|
+
rotatedArchive = archive;
|
|
621
|
+
} else if (stat.size >= LOG_MAX_SIZE_BYTES) {
|
|
622
|
+
renameSync2(logPath, archive);
|
|
623
|
+
rotatedArchive = archive;
|
|
624
|
+
}
|
|
625
|
+
} catch {}
|
|
626
|
+
}
|
|
627
|
+
runCleanup(logDir, rotatedArchive);
|
|
525
628
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
629
|
+
|
|
630
|
+
// src/services/logger.ts
|
|
631
|
+
class Logger {
|
|
632
|
+
enabled;
|
|
633
|
+
logPath;
|
|
634
|
+
constructor() {
|
|
635
|
+
this.enabled = !!(process.env["DEBUG"] || process.env["CC_STATUSLINE_DEBUG"]);
|
|
636
|
+
const logDir = process.env["CC_API_STATUSLINE_LOG_DIR"] || join3(homedir2(), ".claude", "cc-api-statusline");
|
|
637
|
+
this.logPath = join3(logDir, "debug.log");
|
|
638
|
+
if (this.enabled) {
|
|
639
|
+
this.ensureLogDir();
|
|
640
|
+
maybeRotateLogs(this.logPath);
|
|
641
|
+
}
|
|
530
642
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
643
|
+
ensureLogDir() {
|
|
644
|
+
try {
|
|
645
|
+
const dir = dirname3(this.logPath);
|
|
646
|
+
ensureDir(dir);
|
|
647
|
+
} catch {
|
|
648
|
+
this.enabled = false;
|
|
537
649
|
}
|
|
538
|
-
return data;
|
|
539
|
-
} catch (error) {
|
|
540
|
-
console.warn(`Failed to read cache from ${path}: ${error}`);
|
|
541
|
-
return null;
|
|
542
650
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
ensureCacheDir();
|
|
548
|
-
const content = JSON.stringify(entry, null, 2);
|
|
549
|
-
atomicWriteFile(path, content);
|
|
550
|
-
} catch (error) {
|
|
551
|
-
console.warn(`Failed to write cache to ${path}: ${error}`);
|
|
651
|
+
formatLocalTimestamp() {
|
|
652
|
+
const d = new Date;
|
|
653
|
+
const pad = (n, len = 2) => n.toString().padStart(len, "0");
|
|
654
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
552
655
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const ttlMs = entry.ttlSeconds * 1000;
|
|
559
|
-
if (age >= ttlMs) {
|
|
560
|
-
return false;
|
|
656
|
+
format(level, message, data) {
|
|
657
|
+
const timestamp = this.formatLocalTimestamp();
|
|
658
|
+
const dataStr = data ? ` ${JSON.stringify(data)}` : "";
|
|
659
|
+
return `[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}
|
|
660
|
+
`;
|
|
561
661
|
}
|
|
562
|
-
|
|
563
|
-
|
|
662
|
+
write(level, message, data) {
|
|
663
|
+
if (!this.enabled) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
const entry = this.format(level, message, data);
|
|
668
|
+
appendFileSync(this.logPath, entry, { encoding: "utf-8" });
|
|
669
|
+
} catch {}
|
|
564
670
|
}
|
|
565
|
-
|
|
566
|
-
|
|
671
|
+
debug(message, data) {
|
|
672
|
+
this.write("debug", message, data);
|
|
567
673
|
}
|
|
568
|
-
|
|
569
|
-
|
|
674
|
+
info(message, data) {
|
|
675
|
+
this.write("info", message, data);
|
|
676
|
+
}
|
|
677
|
+
warn(message, data) {
|
|
678
|
+
this.write("warn", message, data);
|
|
679
|
+
}
|
|
680
|
+
error(message, data) {
|
|
681
|
+
this.write("error", message, data);
|
|
682
|
+
}
|
|
683
|
+
isEnabled() {
|
|
684
|
+
return this.enabled;
|
|
685
|
+
}
|
|
686
|
+
getLogPath() {
|
|
687
|
+
return this.logPath;
|
|
570
688
|
}
|
|
571
|
-
return true;
|
|
572
689
|
}
|
|
573
|
-
|
|
574
|
-
|
|
690
|
+
var logger = new Logger;
|
|
691
|
+
|
|
692
|
+
// src/services/endpoint-config.ts
|
|
693
|
+
function getEndpointConfigDir(customRoot) {
|
|
694
|
+
const envRoot = process.env["CC_API_STATUSLINE_CONFIG_DIR"];
|
|
695
|
+
const root = customRoot || envRoot || join4(homedir3(), ".claude", "cc-api-statusline");
|
|
696
|
+
return join4(root, "api-config");
|
|
697
|
+
}
|
|
698
|
+
function loadEndpointConfigs(customDir) {
|
|
699
|
+
const configDir = getEndpointConfigDir(customDir);
|
|
700
|
+
if (!existsSync5(configDir)) {
|
|
701
|
+
return getBuiltInEndpointConfigs();
|
|
702
|
+
}
|
|
703
|
+
const registry = {};
|
|
704
|
+
const files = readdirSync2(configDir).filter((f) => f.endsWith(".json"));
|
|
705
|
+
if (files.length === 0) {
|
|
706
|
+
return getBuiltInEndpointConfigs();
|
|
707
|
+
}
|
|
708
|
+
for (const file of files) {
|
|
709
|
+
const filePath = join4(configDir, file);
|
|
710
|
+
try {
|
|
711
|
+
const config = loadEndpointConfigFile(filePath);
|
|
712
|
+
registry[config.provider] = config;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
logger.error(`Failed to load endpoint config ${file}`, { error: String(error) });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (Object.keys(registry).length === 0) {
|
|
718
|
+
return getBuiltInEndpointConfigs();
|
|
719
|
+
}
|
|
720
|
+
return registry;
|
|
575
721
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
722
|
+
function loadEndpointConfigFile(filePath) {
|
|
723
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
724
|
+
const data = JSON.parse(content);
|
|
725
|
+
validateEndpointConfig(data, filePath);
|
|
726
|
+
return data;
|
|
578
727
|
}
|
|
579
|
-
function
|
|
580
|
-
if (
|
|
581
|
-
|
|
728
|
+
function validateEndpointConfig(data, filename) {
|
|
729
|
+
if (typeof data !== "object" || data === null) {
|
|
730
|
+
throw new Error(`${filename}: Config must be an object`);
|
|
582
731
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
732
|
+
const config = data;
|
|
733
|
+
if (typeof config.provider !== "string" || !config.provider) {
|
|
734
|
+
throw new Error(`${filename}: Missing or invalid 'provider' field`);
|
|
735
|
+
}
|
|
736
|
+
if (typeof config.endpoint !== "object" || config.endpoint === null) {
|
|
737
|
+
throw new Error(`${filename}: Missing or invalid 'endpoint' field`);
|
|
738
|
+
}
|
|
739
|
+
const endpoint = config.endpoint;
|
|
740
|
+
if (typeof endpoint.path !== "string" || !endpoint.path) {
|
|
741
|
+
throw new Error(`${filename}: Missing or invalid 'endpoint.path' field`);
|
|
742
|
+
}
|
|
743
|
+
if (endpoint.method !== "GET" && endpoint.method !== "POST") {
|
|
744
|
+
throw new Error(`${filename}: Invalid 'endpoint.method' (must be GET or POST)`);
|
|
745
|
+
}
|
|
746
|
+
if (typeof config.auth !== "object" || config.auth === null) {
|
|
747
|
+
throw new Error(`${filename}: Missing or invalid 'auth' field`);
|
|
748
|
+
}
|
|
749
|
+
const auth = config.auth;
|
|
750
|
+
if (!["bearer-header", "body-key", "custom-header"].includes(auth.type)) {
|
|
751
|
+
throw new Error(`${filename}: Invalid 'auth.type' (must be bearer-header, body-key, or custom-header)`);
|
|
752
|
+
}
|
|
753
|
+
if (typeof config.responseMapping !== "object" || config.responseMapping === null) {
|
|
754
|
+
throw new Error(`${filename}: Missing or invalid 'responseMapping' field`);
|
|
755
|
+
}
|
|
756
|
+
const mapping = config.responseMapping;
|
|
757
|
+
for (const [key, val] of Object.entries(mapping)) {
|
|
758
|
+
if (val !== undefined && typeof val !== "string") {
|
|
759
|
+
throw new Error(`${filename}: responseMapping.${key} must be a string`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (config.displayName !== undefined && typeof config.displayName !== "string") {
|
|
763
|
+
throw new Error(`${filename}: Invalid 'displayName' field (must be string)`);
|
|
764
|
+
}
|
|
765
|
+
if (config.defaults !== undefined && typeof config.defaults !== "object") {
|
|
766
|
+
throw new Error(`${filename}: Invalid 'defaults' field (must be object)`);
|
|
767
|
+
}
|
|
768
|
+
if (config.detection !== undefined && typeof config.detection !== "object") {
|
|
769
|
+
throw new Error(`${filename}: Invalid 'detection' field (must be object)`);
|
|
589
770
|
}
|
|
590
771
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (
|
|
594
|
-
|
|
772
|
+
function computeEndpointConfigHash(customDir) {
|
|
773
|
+
const configDir = getEndpointConfigDir(customDir);
|
|
774
|
+
if (!existsSync5(configDir)) {
|
|
775
|
+
const builtIn = getBuiltInEndpointConfigs();
|
|
776
|
+
const serialized = JSON.stringify(builtIn, Object.keys(builtIn).sort());
|
|
777
|
+
return sha256(serialized).slice(0, 12);
|
|
595
778
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
779
|
+
const files = readdirSync2(configDir).filter((f) => f.endsWith(".json")).sort();
|
|
780
|
+
if (files.length === 0) {
|
|
781
|
+
const builtIn = getBuiltInEndpointConfigs();
|
|
782
|
+
const serialized = JSON.stringify(builtIn, Object.keys(builtIn).sort());
|
|
783
|
+
return sha256(serialized).slice(0, 12);
|
|
784
|
+
}
|
|
785
|
+
let combined = "";
|
|
786
|
+
for (const file of files) {
|
|
787
|
+
const filePath = join4(configDir, file);
|
|
788
|
+
try {
|
|
789
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
790
|
+
combined += `\x00${file}\x00${content}`;
|
|
791
|
+
} catch {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return sha256(combined).slice(0, 12);
|
|
598
796
|
}
|
|
599
|
-
function
|
|
600
|
-
|
|
601
|
-
|
|
797
|
+
function getBuiltInEndpointConfigs() {
|
|
798
|
+
return {
|
|
799
|
+
sub2api: {
|
|
800
|
+
provider: "sub2api",
|
|
801
|
+
displayName: "sub2api",
|
|
802
|
+
endpoint: {
|
|
803
|
+
path: "/v1/usage",
|
|
804
|
+
method: "GET"
|
|
805
|
+
},
|
|
806
|
+
auth: {
|
|
807
|
+
type: "bearer-header"
|
|
808
|
+
},
|
|
809
|
+
defaults: {
|
|
810
|
+
unit: "USD",
|
|
811
|
+
planName: "Unknown"
|
|
812
|
+
},
|
|
813
|
+
detection: {
|
|
814
|
+
healthMatch: { status: "ok" }
|
|
815
|
+
},
|
|
816
|
+
responseMapping: {
|
|
817
|
+
billingMode: "subscription",
|
|
818
|
+
planName: "$.planName",
|
|
819
|
+
"balance.remaining": "$.remaining",
|
|
820
|
+
"balance.unit": "$.unit",
|
|
821
|
+
"daily.used": "$.subscription.daily_usage_usd",
|
|
822
|
+
"daily.limit": "$.subscription.daily_limit_usd",
|
|
823
|
+
"weekly.used": "$.subscription.weekly_usage_usd",
|
|
824
|
+
"weekly.limit": "$.subscription.weekly_limit_usd",
|
|
825
|
+
"monthly.used": "$.subscription.monthly_usage_usd",
|
|
826
|
+
"monthly.limit": "$.subscription.monthly_limit_usd",
|
|
827
|
+
"tokenStats.today.requests": "$.usage.today.requests",
|
|
828
|
+
"tokenStats.today.inputTokens": "$.usage.today.input_tokens",
|
|
829
|
+
"tokenStats.today.outputTokens": "$.usage.today.output_tokens",
|
|
830
|
+
"tokenStats.today.cacheCreationTokens": "$.usage.today.cache_creation_tokens",
|
|
831
|
+
"tokenStats.today.cacheReadTokens": "$.usage.today.cache_read_tokens",
|
|
832
|
+
"tokenStats.today.totalTokens": "$.usage.today.total_tokens",
|
|
833
|
+
"tokenStats.today.cost": "$.usage.today.cost",
|
|
834
|
+
"tokenStats.total.requests": "$.usage.total.requests",
|
|
835
|
+
"tokenStats.total.inputTokens": "$.usage.total.input_tokens",
|
|
836
|
+
"tokenStats.total.outputTokens": "$.usage.total.output_tokens",
|
|
837
|
+
"tokenStats.total.totalTokens": "$.usage.total.total_tokens",
|
|
838
|
+
"tokenStats.total.cost": "$.usage.total.cost",
|
|
839
|
+
"tokenStats.rpm": "$.usage.rpm",
|
|
840
|
+
"tokenStats.tpm": "$.usage.tpm"
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
"claude-relay-service": {
|
|
844
|
+
provider: "claude-relay-service",
|
|
845
|
+
displayName: "CRS",
|
|
846
|
+
endpoint: {
|
|
847
|
+
path: "/apiStats/api/user-stats",
|
|
848
|
+
method: "POST",
|
|
849
|
+
contentType: "application/json"
|
|
850
|
+
},
|
|
851
|
+
auth: {
|
|
852
|
+
type: "body-key",
|
|
853
|
+
bodyField: "apiKey"
|
|
854
|
+
},
|
|
855
|
+
defaults: {
|
|
856
|
+
billingMode: "subscription",
|
|
857
|
+
planName: "API Key",
|
|
858
|
+
resetSemantics: "rolling-window"
|
|
859
|
+
},
|
|
860
|
+
detection: {
|
|
861
|
+
urlPatterns: ["/apistats", "/api/user-stats"],
|
|
862
|
+
healthMatch: { service: "*" }
|
|
863
|
+
},
|
|
864
|
+
responseMapping: {
|
|
865
|
+
billingMode: "subscription",
|
|
866
|
+
planName: "$.data.name",
|
|
867
|
+
"daily.used": "$.data.limits.currentDailyCost",
|
|
868
|
+
"daily.limit": "$.data.limits.dailyCostLimit",
|
|
869
|
+
"weekly.used": "$.data.limits.weeklyOpusCost",
|
|
870
|
+
"weekly.limit": "$.data.limits.weeklyOpusCostLimit",
|
|
871
|
+
"monthly.used": "$.data.limits.currentTotalCost",
|
|
872
|
+
"monthly.limit": "$.data.limits.totalCostLimit",
|
|
873
|
+
"tokenStats.total.requests": "$.data.usage.total.requests",
|
|
874
|
+
"tokenStats.total.inputTokens": "$.data.usage.total.inputTokens",
|
|
875
|
+
"tokenStats.total.outputTokens": "$.data.usage.total.outputTokens",
|
|
876
|
+
"tokenStats.total.cacheCreationTokens": "$.data.usage.total.cacheCreateTokens",
|
|
877
|
+
"tokenStats.total.cacheReadTokens": "$.data.usage.total.cacheReadTokens",
|
|
878
|
+
"tokenStats.total.totalTokens": "$.data.usage.total.tokens",
|
|
879
|
+
"tokenStats.total.cost": "$.data.usage.total.cost",
|
|
880
|
+
"rateLimit.windowSeconds": "$.data.limits.rateLimitWindow",
|
|
881
|
+
"rateLimit.requestsUsed": "$.data.limits.currentWindowRequests",
|
|
882
|
+
"rateLimit.requestsLimit": "$.data.limits.rateLimitRequests",
|
|
883
|
+
"rateLimit.costUsed": "$.data.limits.currentWindowCost",
|
|
884
|
+
"rateLimit.costLimit": "$.data.limits.rateLimitCost",
|
|
885
|
+
"rateLimit.remainingSeconds": "$.data.limits.windowRemainingSeconds"
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
};
|
|
602
889
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
890
|
+
|
|
891
|
+
// src/services/endpoint-lock.ts
|
|
892
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
893
|
+
import { join as join5 } from "path";
|
|
894
|
+
import { homedir as homedir4 } from "os";
|
|
895
|
+
function getLockFilePath(customDir) {
|
|
896
|
+
if (customDir) {
|
|
897
|
+
return join5(customDir, ".endpoint-config.lock");
|
|
898
|
+
}
|
|
899
|
+
return join5(homedir4(), ".claude", "cc-api-statusline", ".endpoint-config.lock");
|
|
900
|
+
}
|
|
901
|
+
function readEndpointLock(customDir) {
|
|
902
|
+
const lockPath = getLockFilePath(customDir);
|
|
903
|
+
if (!existsSync6(lockPath)) {
|
|
606
904
|
return null;
|
|
607
905
|
}
|
|
608
906
|
try {
|
|
609
|
-
const content =
|
|
907
|
+
const content = readFileSync4(lockPath, "utf-8");
|
|
610
908
|
const data = JSON.parse(content);
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const now = Date.now();
|
|
617
|
-
const age = now - detectedAt;
|
|
618
|
-
const ttlMs = data.ttlSeconds * 1000;
|
|
619
|
-
if (age >= ttlMs) {
|
|
620
|
-
try {
|
|
621
|
-
unlinkSync2(path);
|
|
622
|
-
} catch {}
|
|
623
|
-
return null;
|
|
909
|
+
if (typeof data === "object" && data !== null && "hash" in data && "lockedAt" in data && typeof data.hash === "string" && typeof data.lockedAt === "string") {
|
|
910
|
+
return {
|
|
911
|
+
hash: data.hash,
|
|
912
|
+
lockedAt: data.lockedAt
|
|
913
|
+
};
|
|
624
914
|
}
|
|
625
|
-
return
|
|
626
|
-
} catch
|
|
627
|
-
console.warn(`Failed to read provider detection cache from ${path}: ${error}`);
|
|
915
|
+
return null;
|
|
916
|
+
} catch {
|
|
628
917
|
return null;
|
|
629
918
|
}
|
|
630
919
|
}
|
|
631
|
-
function
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
920
|
+
function writeEndpointLock(hash, customDir) {
|
|
921
|
+
const lockPath = getLockFilePath(customDir);
|
|
922
|
+
const entry = {
|
|
923
|
+
hash,
|
|
924
|
+
lockedAt: new Date().toISOString()
|
|
925
|
+
};
|
|
926
|
+
atomicWriteFile(lockPath, JSON.stringify(entry, null, 2), {
|
|
927
|
+
ensureParentDir: true,
|
|
928
|
+
appendNewline: true
|
|
929
|
+
});
|
|
640
930
|
}
|
|
641
931
|
|
|
642
|
-
// src/services/config.ts
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
let pipedRequestTimeoutMs = config.pipedRequestTimeoutMs;
|
|
672
|
-
if (maxWidth < 20) {
|
|
673
|
-
console.warn("Warning: display.maxWidth < 20, clamping to 20");
|
|
674
|
-
maxWidth = 20;
|
|
675
|
-
}
|
|
676
|
-
if (maxWidth > 100) {
|
|
677
|
-
console.warn("Warning: display.maxWidth > 100, clamping to 100");
|
|
678
|
-
maxWidth = 100;
|
|
932
|
+
// src/services/config-defaults.ts
|
|
933
|
+
function getDefaultStyleConfig() {
|
|
934
|
+
return DEFAULT_CONFIG;
|
|
935
|
+
}
|
|
936
|
+
function getDefaultSub2apiConfig() {
|
|
937
|
+
const configs = getBuiltInEndpointConfigs();
|
|
938
|
+
const config = configs["sub2api"];
|
|
939
|
+
if (!config)
|
|
940
|
+
throw new Error("Built-in sub2api config not found");
|
|
941
|
+
return config;
|
|
942
|
+
}
|
|
943
|
+
function getDefaultCrsConfig() {
|
|
944
|
+
const configs = getBuiltInEndpointConfigs();
|
|
945
|
+
const config = configs["claude-relay-service"];
|
|
946
|
+
if (!config)
|
|
947
|
+
throw new Error("Built-in claude-relay-service config not found");
|
|
948
|
+
return config;
|
|
949
|
+
}
|
|
950
|
+
function writeDefaultConfigs(customDir) {
|
|
951
|
+
const configDir = customDir || join6(homedir5(), ".claude", "cc-api-statusline");
|
|
952
|
+
const configPath = join6(configDir, "config.json");
|
|
953
|
+
const apiConfigDir = getEndpointConfigDir(customDir);
|
|
954
|
+
ensureDir(configDir);
|
|
955
|
+
ensureDir(apiConfigDir);
|
|
956
|
+
if (!existsSync7(configPath)) {
|
|
957
|
+
const styleConfig = getDefaultStyleConfig();
|
|
958
|
+
atomicWriteFile(configPath, JSON.stringify(styleConfig, null, 2), {
|
|
959
|
+
appendNewline: true
|
|
960
|
+
});
|
|
679
961
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
962
|
+
const sub2apiPath = join6(apiConfigDir, "sub2api.json");
|
|
963
|
+
if (!existsSync7(sub2apiPath)) {
|
|
964
|
+
const sub2apiConfig = getDefaultSub2apiConfig();
|
|
965
|
+
atomicWriteFile(sub2apiPath, JSON.stringify(sub2apiConfig, null, 2), {
|
|
966
|
+
appendNewline: true
|
|
967
|
+
});
|
|
683
968
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
969
|
+
const crsPath = join6(apiConfigDir, "crs.json");
|
|
970
|
+
if (!existsSync7(crsPath)) {
|
|
971
|
+
const crsConfig = getDefaultCrsConfig();
|
|
972
|
+
atomicWriteFile(crsPath, JSON.stringify(crsConfig, null, 2), {
|
|
973
|
+
appendNewline: true
|
|
974
|
+
});
|
|
687
975
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
display: {
|
|
691
|
-
...config.display,
|
|
692
|
-
maxWidth
|
|
693
|
-
},
|
|
694
|
-
pollIntervalSeconds,
|
|
695
|
-
pipedRequestTimeoutMs
|
|
696
|
-
};
|
|
976
|
+
const currentHash = computeEndpointConfigHash(customDir);
|
|
977
|
+
writeEndpointLock(currentHash, customDir);
|
|
697
978
|
}
|
|
698
|
-
function
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
const content = readFileSync4(path, "utf-8");
|
|
705
|
-
const userConfig = JSON.parse(content);
|
|
706
|
-
const merged = deepMerge(DEFAULT_CONFIG, userConfig);
|
|
707
|
-
return validateConfig(merged);
|
|
708
|
-
} catch (error) {
|
|
709
|
-
console.warn(`Warning: Could not load config from ${path}: ${error}`);
|
|
710
|
-
console.warn("Using default configuration");
|
|
711
|
-
return DEFAULT_CONFIG;
|
|
712
|
-
}
|
|
979
|
+
function needsConfigInit(customDir) {
|
|
980
|
+
const configDir = customDir || join6(homedir5(), ".claude", "cc-api-statusline");
|
|
981
|
+
const configPath = join6(configDir, "config.json");
|
|
982
|
+
const apiConfigDir = getEndpointConfigDir(customDir);
|
|
983
|
+
return !existsSync7(configPath) || !existsSync7(apiConfigDir);
|
|
713
984
|
}
|
|
714
985
|
|
|
715
986
|
// src/providers/http.ts
|
|
@@ -816,76 +1087,419 @@ async function secureFetch(url, options = {}, timeoutMs = 5000, userAgent) {
|
|
|
816
1087
|
}
|
|
817
1088
|
}
|
|
818
1089
|
|
|
819
|
-
// src/
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1090
|
+
// src/providers/health-probe.ts
|
|
1091
|
+
function extractOrigin(baseUrl) {
|
|
1092
|
+
try {
|
|
1093
|
+
const url = new URL(baseUrl);
|
|
1094
|
+
return url.origin;
|
|
1095
|
+
} catch {
|
|
1096
|
+
return baseUrl;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
1100
|
+
const origin = extractOrigin(baseUrl);
|
|
1101
|
+
const healthUrl = `${origin}/health`;
|
|
1102
|
+
logger.debug("Probing health endpoint", { healthUrl, timeoutMs });
|
|
1103
|
+
try {
|
|
1104
|
+
const responseText = await secureFetch(healthUrl, {
|
|
1105
|
+
method: "GET",
|
|
1106
|
+
headers: {
|
|
1107
|
+
Accept: "application/json"
|
|
1108
|
+
}
|
|
1109
|
+
}, timeoutMs);
|
|
1110
|
+
const data = JSON.parse(responseText);
|
|
1111
|
+
logger.debug("Health probe response", { data });
|
|
1112
|
+
if (typeof data["service"] === "string") {
|
|
1113
|
+
logger.debug("Detected provider from service field", { provider: data["service"] });
|
|
1114
|
+
return data["service"];
|
|
1115
|
+
}
|
|
1116
|
+
if (data["status"] === "ok") {
|
|
1117
|
+
logger.debug("Detected sub2api from status: ok pattern");
|
|
1118
|
+
return "sub2api";
|
|
1119
|
+
}
|
|
1120
|
+
logger.debug("Health probe returned unrecognized pattern", { data });
|
|
1121
|
+
return null;
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
logger.debug("Health probe failed", { error: String(error) });
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
823
1127
|
|
|
824
|
-
// src/services/
|
|
825
|
-
import {
|
|
826
|
-
import { join as
|
|
827
|
-
import { homedir as
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1128
|
+
// src/services/cache.ts
|
|
1129
|
+
import { readFileSync as readFileSync5, existsSync as existsSync8, unlinkSync as unlinkSync4 } from "fs";
|
|
1130
|
+
import { join as join7 } from "path";
|
|
1131
|
+
import { homedir as homedir6 } from "os";
|
|
1132
|
+
function getCacheDir() {
|
|
1133
|
+
const override = process.env["CC_API_STATUSLINE_CACHE_DIR"];
|
|
1134
|
+
if (override) {
|
|
1135
|
+
return override;
|
|
1136
|
+
}
|
|
1137
|
+
return join7(homedir6(), ".claude", "cc-api-statusline");
|
|
1138
|
+
}
|
|
1139
|
+
function ensureCacheDir() {
|
|
1140
|
+
const dir = getCacheDir();
|
|
1141
|
+
ensureDir(dir);
|
|
1142
|
+
}
|
|
1143
|
+
function getCachePath(baseUrl) {
|
|
1144
|
+
const hash = shortHash(baseUrl, 12);
|
|
1145
|
+
return join7(getCacheDir(), `cache-${hash}.json`);
|
|
1146
|
+
}
|
|
1147
|
+
function readCache(baseUrl) {
|
|
1148
|
+
const path = getCachePath(baseUrl);
|
|
1149
|
+
if (!existsSync8(path)) {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
try {
|
|
1153
|
+
const content = readFileSync5(path, "utf-8");
|
|
1154
|
+
const data = JSON.parse(content);
|
|
1155
|
+
if (!isCacheEntry(data)) {
|
|
1156
|
+
console.warn(`Invalid cache structure at ${path}`);
|
|
1157
|
+
return null;
|
|
837
1158
|
}
|
|
1159
|
+
return data;
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
console.warn(`Failed to read cache from ${path}: ${error}`);
|
|
1162
|
+
return null;
|
|
838
1163
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1164
|
+
}
|
|
1165
|
+
function writeCache(baseUrl, entry) {
|
|
1166
|
+
const path = getCachePath(baseUrl);
|
|
1167
|
+
try {
|
|
1168
|
+
ensureCacheDir();
|
|
1169
|
+
const content = JSON.stringify(entry, null, 2);
|
|
1170
|
+
atomicWriteFile(path, content);
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
console.warn(`Failed to write cache to ${path}: ${error}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function isCacheValid(entry, currentEnv) {
|
|
1176
|
+
const fetchedAt = new Date(entry.fetchedAt).getTime();
|
|
1177
|
+
const now = Date.now();
|
|
1178
|
+
const age = now - fetchedAt;
|
|
1179
|
+
const ttlMs = entry.ttlSeconds * 1000;
|
|
1180
|
+
if (age >= ttlMs) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
if (entry.baseUrl !== currentEnv.baseUrl) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
if (entry.version !== CACHE_VERSION) {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
if (entry.tokenHash !== currentEnv.tokenHash) {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
function isCacheProviderValid(entry, currentProvider) {
|
|
1195
|
+
return entry.provider === currentProvider;
|
|
1196
|
+
}
|
|
1197
|
+
function isCacheRenderedLineUsable(entry, currentConfigHash) {
|
|
1198
|
+
return entry.configHash === currentConfigHash;
|
|
1199
|
+
}
|
|
1200
|
+
function computeConfigHash(configPath) {
|
|
1201
|
+
if (!existsSync8(configPath)) {
|
|
1202
|
+
return sha256("").slice(0, 12);
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
const bytes = readFileSync5(configPath);
|
|
1206
|
+
return shortHash(bytes.toString("utf-8"), 12);
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
console.warn(`Failed to read config for hash: ${error}`);
|
|
1209
|
+
return sha256("").slice(0, 12);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
var DEFAULT_POLL_INTERVAL_SECONDS = 30;
|
|
1213
|
+
function getEffectivePollInterval(config, envOverride) {
|
|
1214
|
+
if (envOverride !== null) {
|
|
1215
|
+
return Math.max(5, envOverride);
|
|
1216
|
+
}
|
|
1217
|
+
const fromConfig = config.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS;
|
|
1218
|
+
return Math.max(5, fromConfig);
|
|
1219
|
+
}
|
|
1220
|
+
function getProviderDetectionCachePath(baseUrl) {
|
|
1221
|
+
const hash = shortHash(baseUrl, 12);
|
|
1222
|
+
return join7(getCacheDir(), `provider-detect-${hash}.json`);
|
|
1223
|
+
}
|
|
1224
|
+
function readProviderDetectionCache(baseUrl) {
|
|
1225
|
+
const path = getProviderDetectionCachePath(baseUrl);
|
|
1226
|
+
if (!existsSync8(path)) {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
const content = readFileSync5(path, "utf-8");
|
|
1231
|
+
const data = JSON.parse(content);
|
|
1232
|
+
if (!isProviderDetectionCacheEntry(data)) {
|
|
1233
|
+
console.warn(`Invalid provider detection cache structure at ${path}`);
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
const detectedAt = new Date(data.detectedAt).getTime();
|
|
1237
|
+
const now = Date.now();
|
|
1238
|
+
const age = now - detectedAt;
|
|
1239
|
+
const ttlMs = data.ttlSeconds * 1000;
|
|
1240
|
+
if (age >= ttlMs) {
|
|
1241
|
+
try {
|
|
1242
|
+
unlinkSync4(path);
|
|
1243
|
+
} catch {}
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
return data;
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
console.warn(`Failed to read provider detection cache from ${path}: ${error}`);
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function writeProviderDetectionCache(baseUrl, entry) {
|
|
1253
|
+
const path = getProviderDetectionCachePath(baseUrl);
|
|
1254
|
+
try {
|
|
1255
|
+
ensureCacheDir();
|
|
1256
|
+
const content = JSON.stringify(entry, null, 2);
|
|
1257
|
+
atomicWriteFile(path, content);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
console.warn(`Failed to write provider detection cache to ${path}: ${error}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/providers/autodetect.ts
|
|
1264
|
+
var detectionCache = new Map;
|
|
1265
|
+
function detectProviderFromUrlPattern(baseUrl, endpointConfigs = {}) {
|
|
1266
|
+
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1267
|
+
for (const [providerId, config] of Object.entries(endpointConfigs)) {
|
|
1268
|
+
const urlPatterns = config.detection?.urlPatterns;
|
|
1269
|
+
if (urlPatterns && urlPatterns.length > 0) {
|
|
1270
|
+
for (const pattern of urlPatterns) {
|
|
1271
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
1272
|
+
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1273
|
+
return providerId;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
845
1276
|
}
|
|
846
1277
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const pad = (n, len = 2) => n.toString().padStart(len, "0");
|
|
850
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
1278
|
+
if (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats")) {
|
|
1279
|
+
return "claude-relay-service";
|
|
851
1280
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1281
|
+
return "sub2api";
|
|
1282
|
+
}
|
|
1283
|
+
async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {}, probeTimeoutMs = 1500) {
|
|
1284
|
+
if (providerOverride) {
|
|
1285
|
+
logger.debug("Provider override detected", { provider: providerOverride });
|
|
1286
|
+
return providerOverride;
|
|
1287
|
+
}
|
|
1288
|
+
const cached = detectionCache.get(baseUrl);
|
|
1289
|
+
if (cached) {
|
|
1290
|
+
logger.debug("Provider detection cache hit (memory)", { provider: cached.provider });
|
|
1291
|
+
return cached.provider;
|
|
1292
|
+
}
|
|
1293
|
+
const diskCached = readProviderDetectionCache(baseUrl);
|
|
1294
|
+
if (diskCached) {
|
|
1295
|
+
logger.debug("Provider detection cache hit (disk)", {
|
|
1296
|
+
provider: diskCached.provider,
|
|
1297
|
+
detectedVia: diskCached.detectedVia
|
|
1298
|
+
});
|
|
1299
|
+
detectionCache.set(baseUrl, {
|
|
1300
|
+
provider: diskCached.provider,
|
|
1301
|
+
detectedAt: diskCached.detectedAt
|
|
1302
|
+
});
|
|
1303
|
+
return diskCached.provider;
|
|
1304
|
+
}
|
|
1305
|
+
for (const [providerId, config] of Object.entries(endpointConfigs)) {
|
|
1306
|
+
const urlPatterns = config.detection?.urlPatterns;
|
|
1307
|
+
if (urlPatterns && urlPatterns.length > 0) {
|
|
1308
|
+
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1309
|
+
for (const pattern of urlPatterns) {
|
|
1310
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
1311
|
+
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1312
|
+
logger.debug("Provider detected via endpoint URL pattern", { provider: providerId, pattern });
|
|
1313
|
+
cacheProviderDetection(baseUrl, providerId, "url-pattern");
|
|
1314
|
+
return providerId;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
|
|
1320
|
+
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
|
|
1321
|
+
if (probedProvider) {
|
|
1322
|
+
logger.debug("Provider detected via health probe", { provider: probedProvider });
|
|
1323
|
+
cacheProviderDetection(baseUrl, probedProvider, "health-probe");
|
|
1324
|
+
return probedProvider;
|
|
1325
|
+
}
|
|
1326
|
+
const patternProvider = detectProviderFromUrlPattern(baseUrl, {});
|
|
1327
|
+
logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
|
|
1328
|
+
cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
|
|
1329
|
+
return patternProvider;
|
|
1330
|
+
}
|
|
1331
|
+
function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
1332
|
+
const now = new Date().toISOString();
|
|
1333
|
+
detectionCache.set(baseUrl, {
|
|
1334
|
+
provider,
|
|
1335
|
+
detectedAt: now
|
|
1336
|
+
});
|
|
1337
|
+
writeProviderDetectionCache(baseUrl, {
|
|
1338
|
+
baseUrl,
|
|
1339
|
+
provider,
|
|
1340
|
+
detectedVia,
|
|
1341
|
+
detectedAt: now,
|
|
1342
|
+
ttlSeconds: PROVIDER_DETECTION_TTL_SECONDS
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
function clearDetectionCache() {
|
|
1346
|
+
detectionCache.clear();
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/cli/commands.ts
|
|
1350
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
|
|
1351
|
+
import { join as join8 } from "path";
|
|
1352
|
+
function handleInstall(args) {
|
|
1353
|
+
const existing = getExistingStatusLine();
|
|
1354
|
+
if (existing && !args.force) {
|
|
1355
|
+
console.error("Error: statusLine is already configured in settings.json");
|
|
1356
|
+
console.error(`Current command: ${existing}`);
|
|
1357
|
+
console.error("Use --force to overwrite, or --uninstall to remove first.");
|
|
1358
|
+
process.exit(1);
|
|
1359
|
+
}
|
|
1360
|
+
const runner = args.runner ?? (isBunxAvailable() ? "bunx" : "npx");
|
|
1361
|
+
console.log("Creating default configuration files...");
|
|
1362
|
+
writeDefaultConfigs();
|
|
1363
|
+
console.log("✓ Config files created:");
|
|
1364
|
+
console.log(" - ~/.claude/cc-api-statusline/config.json");
|
|
1365
|
+
console.log(" - ~/.claude/cc-api-statusline/api-config/sub2api.json");
|
|
1366
|
+
console.log(" - ~/.claude/cc-api-statusline/api-config/crs.json");
|
|
1367
|
+
console.log(" - ~/.claude/cc-api-statusline/.endpoint-config.lock");
|
|
1368
|
+
installStatusLine(runner);
|
|
1369
|
+
console.log("✓ Statusline installed successfully!");
|
|
1370
|
+
console.log(` Runner: ${runner}`);
|
|
1371
|
+
console.log(` Command: ${runner} -y cc-api-statusline@latest`);
|
|
1372
|
+
console.log(` Config: ~/.claude/settings.json`);
|
|
1373
|
+
process.exit(0);
|
|
1374
|
+
}
|
|
1375
|
+
function handleUninstall() {
|
|
1376
|
+
const existing = getExistingStatusLine();
|
|
1377
|
+
if (!existing) {
|
|
1378
|
+
console.log("No statusLine configuration found in settings.json");
|
|
1379
|
+
process.exit(0);
|
|
1380
|
+
}
|
|
1381
|
+
uninstallStatusLine();
|
|
1382
|
+
console.log("✓ Statusline uninstalled successfully");
|
|
1383
|
+
console.log(" Removed statusLine from ~/.claude/settings.json");
|
|
1384
|
+
process.exit(0);
|
|
1385
|
+
}
|
|
1386
|
+
function handleApplyConfig() {
|
|
1387
|
+
console.log("Applying endpoint configuration changes...");
|
|
1388
|
+
const currentHash = computeEndpointConfigHash();
|
|
1389
|
+
console.log(`Current endpoint config hash: ${currentHash}`);
|
|
1390
|
+
clearDetectionCache();
|
|
1391
|
+
console.log("✓ Provider detection cache cleared");
|
|
1392
|
+
const cacheDir = getCacheDir();
|
|
1393
|
+
let failCount = 0;
|
|
1394
|
+
if (existsSync9(cacheDir)) {
|
|
1395
|
+
const files = readdirSync3(cacheDir).filter((f) => f.startsWith("cache-") && f.endsWith(".json"));
|
|
1396
|
+
for (const file of files) {
|
|
1397
|
+
try {
|
|
1398
|
+
const filePath = join8(cacheDir, file);
|
|
1399
|
+
unlinkSync5(filePath);
|
|
1400
|
+
} catch {
|
|
1401
|
+
failCount++;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (failCount > 0) {
|
|
1405
|
+
console.log(`⚠ Cleared ${files.length - failCount}/${files.length} cache files (${failCount} failed)`);
|
|
1406
|
+
} else {
|
|
1407
|
+
console.log(`✓ Cleared ${files.length} data cache file(s)`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
writeEndpointLock(currentHash);
|
|
1411
|
+
console.log("✓ Lock file updated");
|
|
1412
|
+
console.log("");
|
|
1413
|
+
console.log("✓ Endpoint config changes applied successfully!");
|
|
1414
|
+
console.log(" Changes will take effect on next statusline refresh.");
|
|
1415
|
+
console.log("");
|
|
1416
|
+
console.log("Config files:");
|
|
1417
|
+
const apiConfigDir = getEndpointConfigDir();
|
|
1418
|
+
if (existsSync9(apiConfigDir)) {
|
|
1419
|
+
const configFiles = readdirSync3(apiConfigDir).filter((f) => f.endsWith(".json"));
|
|
1420
|
+
for (const file of configFiles) {
|
|
1421
|
+
console.log(` - ${apiConfigDir}/${file}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
process.exit(0);
|
|
1425
|
+
}
|
|
1426
|
+
// src/services/config.ts
|
|
1427
|
+
import { readFileSync as readFileSync6, existsSync as existsSync10 } from "fs";
|
|
1428
|
+
import { join as join9 } from "path";
|
|
1429
|
+
import { homedir as homedir7 } from "os";
|
|
1430
|
+
function getConfigDir() {
|
|
1431
|
+
return join9(homedir7(), ".claude", "cc-api-statusline");
|
|
1432
|
+
}
|
|
1433
|
+
function getConfigPath(customPath) {
|
|
1434
|
+
if (customPath) {
|
|
1435
|
+
return customPath;
|
|
857
1436
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1437
|
+
return join9(getConfigDir(), "config.json");
|
|
1438
|
+
}
|
|
1439
|
+
function deepMerge(target, source) {
|
|
1440
|
+
const result = { ...target };
|
|
1441
|
+
for (const key in source) {
|
|
1442
|
+
const sourceValue = source[key];
|
|
1443
|
+
const targetValue = result[key];
|
|
1444
|
+
if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
|
|
1445
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
1446
|
+
} else if (sourceValue !== undefined) {
|
|
1447
|
+
result[key] = sourceValue;
|
|
861
1448
|
}
|
|
862
|
-
try {
|
|
863
|
-
const entry = this.format(level, message, data);
|
|
864
|
-
appendFileSync(this.logPath, entry, { encoding: "utf-8" });
|
|
865
|
-
} catch {}
|
|
866
1449
|
}
|
|
867
|
-
|
|
868
|
-
|
|
1450
|
+
return result;
|
|
1451
|
+
}
|
|
1452
|
+
function validateConfig(config) {
|
|
1453
|
+
let maxWidth = config.display.maxWidth;
|
|
1454
|
+
let pollIntervalSeconds = config.pollIntervalSeconds;
|
|
1455
|
+
let pipedRequestTimeoutMs = config.pipedRequestTimeoutMs;
|
|
1456
|
+
if (maxWidth < 20) {
|
|
1457
|
+
console.warn("Warning: display.maxWidth < 20, clamping to 20");
|
|
1458
|
+
maxWidth = 20;
|
|
869
1459
|
}
|
|
870
|
-
|
|
871
|
-
|
|
1460
|
+
if (maxWidth > 100) {
|
|
1461
|
+
console.warn("Warning: display.maxWidth > 100, clamping to 100");
|
|
1462
|
+
maxWidth = 100;
|
|
872
1463
|
}
|
|
873
|
-
|
|
874
|
-
|
|
1464
|
+
if (pollIntervalSeconds !== undefined && pollIntervalSeconds < 5) {
|
|
1465
|
+
console.warn("Warning: pollIntervalSeconds < 5, clamping to 5");
|
|
1466
|
+
pollIntervalSeconds = 5;
|
|
875
1467
|
}
|
|
876
|
-
|
|
877
|
-
|
|
1468
|
+
if (pipedRequestTimeoutMs !== undefined && pipedRequestTimeoutMs < 100) {
|
|
1469
|
+
console.warn("Warning: pipedRequestTimeoutMs < 100, clamping to 100");
|
|
1470
|
+
pipedRequestTimeoutMs = 100;
|
|
878
1471
|
}
|
|
879
|
-
|
|
880
|
-
|
|
1472
|
+
return {
|
|
1473
|
+
...config,
|
|
1474
|
+
display: {
|
|
1475
|
+
...config.display,
|
|
1476
|
+
maxWidth
|
|
1477
|
+
},
|
|
1478
|
+
pollIntervalSeconds,
|
|
1479
|
+
pipedRequestTimeoutMs
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function loadConfig(configPath) {
|
|
1483
|
+
const path = getConfigPath(configPath);
|
|
1484
|
+
if (!existsSync10(path)) {
|
|
1485
|
+
return DEFAULT_CONFIG;
|
|
881
1486
|
}
|
|
882
|
-
|
|
883
|
-
|
|
1487
|
+
try {
|
|
1488
|
+
const content = readFileSync6(path, "utf-8");
|
|
1489
|
+
const userConfig = JSON.parse(content);
|
|
1490
|
+
const merged = deepMerge(DEFAULT_CONFIG, userConfig);
|
|
1491
|
+
return validateConfig(merged);
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
console.warn(`Warning: Could not load config from ${path}: ${error}`);
|
|
1494
|
+
console.warn("Using default configuration");
|
|
1495
|
+
return DEFAULT_CONFIG;
|
|
884
1496
|
}
|
|
885
1497
|
}
|
|
886
|
-
var logger = new Logger;
|
|
887
1498
|
|
|
888
1499
|
// src/services/user-agent.ts
|
|
1500
|
+
import { execSync as execSync2 } from "child_process";
|
|
1501
|
+
import { join as join10 } from "path";
|
|
1502
|
+
import { homedir as homedir8 } from "os";
|
|
889
1503
|
var FALLBACK_UA = "claude-cli/2.1.56 (external, cli)";
|
|
890
1504
|
function resolveUserAgent(config) {
|
|
891
1505
|
if (!config) {
|
|
@@ -912,7 +1526,7 @@ function detectClaudeVersion() {
|
|
|
912
1526
|
if (!process.env["CLAUDECODE"]) {
|
|
913
1527
|
return null;
|
|
914
1528
|
}
|
|
915
|
-
const claudePath =
|
|
1529
|
+
const claudePath = join10(homedir8(), ".claude", "bin", "claude");
|
|
916
1530
|
const result = execSync2(`"${claudePath}" --version`, {
|
|
917
1531
|
encoding: "utf-8",
|
|
918
1532
|
timeout: 1000,
|
|
@@ -942,15 +1556,6 @@ function createQuotaWindow(used, limit, resetsAt) {
|
|
|
942
1556
|
};
|
|
943
1557
|
}
|
|
944
1558
|
|
|
945
|
-
// src/core/constants.ts
|
|
946
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 5000;
|
|
947
|
-
var EXIT_BUFFER_MS = 50;
|
|
948
|
-
var STALENESS_THRESHOLD_MINUTES = 5;
|
|
949
|
-
var VERY_STALE_THRESHOLD_MINUTES = 30;
|
|
950
|
-
var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
951
|
-
var GC_MAX_CACHE_FILES = 20;
|
|
952
|
-
var GC_ORPHAN_TMP_AGE_MS = 60 * 60 * 1000;
|
|
953
|
-
|
|
954
1559
|
// src/providers/sub2api.ts
|
|
955
1560
|
function mapPeriodTokens(data) {
|
|
956
1561
|
if (!data)
|
|
@@ -1027,44 +1632,6 @@ async function fetchSub2api(baseUrl, token, config, timeoutMs = DEFAULT_FETCH_TI
|
|
|
1027
1632
|
};
|
|
1028
1633
|
}
|
|
1029
1634
|
|
|
1030
|
-
// src/providers/health-probe.ts
|
|
1031
|
-
function extractOrigin(baseUrl) {
|
|
1032
|
-
try {
|
|
1033
|
-
const url = new URL(baseUrl);
|
|
1034
|
-
return url.origin;
|
|
1035
|
-
} catch {
|
|
1036
|
-
return baseUrl;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
1040
|
-
const origin = extractOrigin(baseUrl);
|
|
1041
|
-
const healthUrl = `${origin}/health`;
|
|
1042
|
-
logger.debug("Probing health endpoint", { healthUrl, timeoutMs });
|
|
1043
|
-
try {
|
|
1044
|
-
const responseText = await secureFetch(healthUrl, {
|
|
1045
|
-
method: "GET",
|
|
1046
|
-
headers: {
|
|
1047
|
-
Accept: "application/json"
|
|
1048
|
-
}
|
|
1049
|
-
}, timeoutMs);
|
|
1050
|
-
const data = JSON.parse(responseText);
|
|
1051
|
-
logger.debug("Health probe response", { data });
|
|
1052
|
-
if (typeof data["service"] === "string") {
|
|
1053
|
-
logger.debug("Detected provider from service field", { provider: data["service"] });
|
|
1054
|
-
return data["service"];
|
|
1055
|
-
}
|
|
1056
|
-
if (data["status"] === "ok") {
|
|
1057
|
-
logger.debug("Detected sub2api from status: ok pattern");
|
|
1058
|
-
return "sub2api";
|
|
1059
|
-
}
|
|
1060
|
-
logger.debug("Health probe returned unrecognized pattern", { data });
|
|
1061
|
-
return null;
|
|
1062
|
-
} catch (error) {
|
|
1063
|
-
logger.debug("Health probe failed", { error: String(error) });
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
1635
|
// src/services/time.ts
|
|
1069
1636
|
function computeNextMidnightLocal() {
|
|
1070
1637
|
const now = new Date;
|
|
@@ -1158,7 +1725,7 @@ async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = DEFAU
|
|
|
1158
1725
|
};
|
|
1159
1726
|
}
|
|
1160
1727
|
|
|
1161
|
-
// src/providers/
|
|
1728
|
+
// src/providers/response-mapping.ts
|
|
1162
1729
|
function resolveJsonPath(data, path) {
|
|
1163
1730
|
if (!path.startsWith("$.")) {
|
|
1164
1731
|
return path;
|
|
@@ -1236,11 +1803,11 @@ function extractTokenStatsPeriod(data, mapping, prefix) {
|
|
|
1236
1803
|
cost: extractNumber(data, mapping[`${prefix}.cost`]) ?? 0
|
|
1237
1804
|
};
|
|
1238
1805
|
}
|
|
1239
|
-
function mapResponseToUsage(responseData, mapping,
|
|
1806
|
+
function mapResponseToUsage(responseData, mapping, endpointConfig) {
|
|
1240
1807
|
const billingModeStr = extractString(responseData, mapping.billingMode, "subscription");
|
|
1241
1808
|
const billingMode = billingModeStr === "balance" ? "balance" : "subscription";
|
|
1242
|
-
const planName = extractString(responseData, mapping.planName,
|
|
1243
|
-
const base = createEmptyNormalizedUsage(
|
|
1809
|
+
const planName = extractString(responseData, mapping.planName, endpointConfig.displayName ?? endpointConfig.provider);
|
|
1810
|
+
const base = createEmptyNormalizedUsage(endpointConfig.provider, billingMode, planName);
|
|
1244
1811
|
const balance = mapping["balance.remaining"] ? (() => {
|
|
1245
1812
|
const remaining = extractNumber(responseData, mapping["balance.remaining"]);
|
|
1246
1813
|
if (remaining === null)
|
|
@@ -1303,159 +1870,75 @@ function mapResponseToUsage(responseData, mapping, providerConfig) {
|
|
|
1303
1870
|
};
|
|
1304
1871
|
}
|
|
1305
1872
|
|
|
1306
|
-
// src/providers/
|
|
1307
|
-
function
|
|
1308
|
-
if (!
|
|
1309
|
-
return "
|
|
1310
|
-
if (!
|
|
1311
|
-
return "
|
|
1312
|
-
if (!
|
|
1313
|
-
return "
|
|
1314
|
-
if (!
|
|
1315
|
-
return "
|
|
1316
|
-
if (!
|
|
1317
|
-
return "
|
|
1318
|
-
if (!
|
|
1319
|
-
return "
|
|
1320
|
-
}
|
|
1321
|
-
if (!
|
|
1322
|
-
return "
|
|
1323
|
-
}
|
|
1324
|
-
if (
|
|
1325
|
-
return '
|
|
1326
|
-
}
|
|
1327
|
-
if (providerConfig.auth.type === "body" && !providerConfig.auth.bodyField) {
|
|
1328
|
-
return 'Custom provider auth.type="body" requires auth.bodyField';
|
|
1329
|
-
}
|
|
1330
|
-
if (providerConfig.urlPatterns && !Array.isArray(providerConfig.urlPatterns)) {
|
|
1331
|
-
return "Custom provider urlPatterns must be an array";
|
|
1873
|
+
// src/providers/endpoint-fetch.ts
|
|
1874
|
+
function validateEndpointConfig2(config) {
|
|
1875
|
+
if (!config.provider)
|
|
1876
|
+
return "Endpoint config missing required field: provider";
|
|
1877
|
+
if (!config.endpoint?.path)
|
|
1878
|
+
return "Endpoint config missing required field: endpoint.path";
|
|
1879
|
+
if (!config.endpoint?.method)
|
|
1880
|
+
return "Endpoint config missing required field: endpoint.method";
|
|
1881
|
+
if (!config.auth)
|
|
1882
|
+
return "Endpoint config missing required field: auth";
|
|
1883
|
+
if (!config.responseMapping)
|
|
1884
|
+
return "Endpoint config missing required field: responseMapping";
|
|
1885
|
+
if (!config.endpoint.path.startsWith("/")) {
|
|
1886
|
+
return "Endpoint path must start with /";
|
|
1887
|
+
}
|
|
1888
|
+
if (config.auth.type === "custom-header" && !config.auth.header) {
|
|
1889
|
+
return 'Auth type="custom-header" requires auth.header';
|
|
1890
|
+
}
|
|
1891
|
+
if (config.auth.type === "body-key" && !config.auth.bodyField) {
|
|
1892
|
+
return 'Auth type="body-key" requires auth.bodyField';
|
|
1332
1893
|
}
|
|
1333
1894
|
return null;
|
|
1334
1895
|
}
|
|
1335
|
-
async function
|
|
1336
|
-
const validationError =
|
|
1896
|
+
async function fetchEndpoint(baseUrl, token, appConfig, endpointConfig, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
1897
|
+
const validationError = validateEndpointConfig2(endpointConfig);
|
|
1337
1898
|
if (validationError) {
|
|
1338
|
-
throw new Error(`Invalid
|
|
1899
|
+
throw new Error(`Invalid endpoint config: ${validationError}`);
|
|
1339
1900
|
}
|
|
1340
|
-
const url = `${baseUrl}${
|
|
1901
|
+
const url = `${baseUrl}${endpointConfig.endpoint.path}`;
|
|
1341
1902
|
const headers = {
|
|
1342
1903
|
Accept: "application/json"
|
|
1343
1904
|
};
|
|
1344
|
-
if (
|
|
1345
|
-
headers["Content-Type"] =
|
|
1905
|
+
if (endpointConfig.endpoint.contentType) {
|
|
1906
|
+
headers["Content-Type"] = endpointConfig.endpoint.contentType;
|
|
1346
1907
|
}
|
|
1347
|
-
if (
|
|
1348
|
-
const prefix =
|
|
1349
|
-
headers[
|
|
1908
|
+
if (endpointConfig.auth.type === "bearer-header") {
|
|
1909
|
+
const prefix = endpointConfig.auth.prefix ?? "Bearer ";
|
|
1910
|
+
headers["Authorization"] = `${prefix}${token}`;
|
|
1911
|
+
} else if (endpointConfig.auth.type === "custom-header" && endpointConfig.auth.header) {
|
|
1912
|
+
const prefix = endpointConfig.auth.prefix ?? "";
|
|
1913
|
+
headers[endpointConfig.auth.header] = `${prefix}${token}`;
|
|
1350
1914
|
}
|
|
1351
1915
|
let body;
|
|
1352
|
-
if (
|
|
1353
|
-
if (
|
|
1354
|
-
const bodyObj = { ...
|
|
1355
|
-
bodyObj[
|
|
1916
|
+
if (endpointConfig.endpoint.method === "POST") {
|
|
1917
|
+
if (endpointConfig.auth.type === "body-key" && endpointConfig.auth.bodyField) {
|
|
1918
|
+
const bodyObj = { ...endpointConfig.requestBody ?? {} };
|
|
1919
|
+
bodyObj[endpointConfig.auth.bodyField] = token;
|
|
1356
1920
|
body = JSON.stringify(bodyObj);
|
|
1357
|
-
} else if (
|
|
1358
|
-
body = JSON.stringify(
|
|
1921
|
+
} else if (endpointConfig.requestBody) {
|
|
1922
|
+
body = JSON.stringify(endpointConfig.requestBody);
|
|
1359
1923
|
}
|
|
1360
1924
|
}
|
|
1361
|
-
const
|
|
1925
|
+
const endpointUA = endpointConfig.spoofClaudeCodeUA;
|
|
1362
1926
|
const globalUA = appConfig.spoofClaudeCodeUA;
|
|
1363
|
-
const effectiveUA =
|
|
1927
|
+
const effectiveUA = endpointUA !== undefined ? endpointUA : globalUA;
|
|
1364
1928
|
const resolvedUA = resolveUserAgent(effectiveUA);
|
|
1365
1929
|
if (resolvedUA) {
|
|
1366
|
-
logger.debug(`Using User-Agent for ${
|
|
1930
|
+
logger.debug(`Using User-Agent for ${endpointConfig.provider}: ${resolvedUA}`);
|
|
1367
1931
|
}
|
|
1368
1932
|
const responseText = await secureFetch(url, {
|
|
1369
|
-
method:
|
|
1933
|
+
method: endpointConfig.endpoint.method,
|
|
1370
1934
|
headers,
|
|
1371
1935
|
body
|
|
1372
1936
|
}, timeoutMs, resolvedUA);
|
|
1373
1937
|
const responseData = JSON.parse(responseText);
|
|
1374
|
-
const result = mapResponseToUsage(responseData,
|
|
1938
|
+
const result = mapResponseToUsage(responseData, endpointConfig.responseMapping, endpointConfig);
|
|
1375
1939
|
return result;
|
|
1376
1940
|
}
|
|
1377
1941
|
|
|
1378
|
-
// src/providers/autodetect.ts
|
|
1379
|
-
var detectionCache = new Map;
|
|
1380
|
-
function detectProviderFromUrlPattern(baseUrl, customProviders = {}) {
|
|
1381
|
-
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1382
|
-
for (const [providerId, config] of Object.entries(customProviders)) {
|
|
1383
|
-
if (config.urlPatterns && config.urlPatterns.length > 0) {
|
|
1384
|
-
for (const pattern of config.urlPatterns) {
|
|
1385
|
-
const normalizedPattern = pattern.toLowerCase();
|
|
1386
|
-
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1387
|
-
return providerId;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
if (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats")) {
|
|
1393
|
-
return "claude-relay-service";
|
|
1394
|
-
}
|
|
1395
|
-
return "sub2api";
|
|
1396
|
-
}
|
|
1397
|
-
async function resolveProvider(baseUrl, providerOverride, customProviders = {}, probeTimeoutMs = 1500) {
|
|
1398
|
-
if (providerOverride) {
|
|
1399
|
-
logger.debug("Provider override detected", { provider: providerOverride });
|
|
1400
|
-
return providerOverride;
|
|
1401
|
-
}
|
|
1402
|
-
const cached = detectionCache.get(baseUrl);
|
|
1403
|
-
if (cached) {
|
|
1404
|
-
logger.debug("Provider detection cache hit (memory)", { provider: cached.provider });
|
|
1405
|
-
return cached.provider;
|
|
1406
|
-
}
|
|
1407
|
-
const diskCached = readProviderDetectionCache(baseUrl);
|
|
1408
|
-
if (diskCached) {
|
|
1409
|
-
logger.debug("Provider detection cache hit (disk)", {
|
|
1410
|
-
provider: diskCached.provider,
|
|
1411
|
-
detectedVia: diskCached.detectedVia
|
|
1412
|
-
});
|
|
1413
|
-
detectionCache.set(baseUrl, {
|
|
1414
|
-
provider: diskCached.provider,
|
|
1415
|
-
detectedAt: diskCached.detectedAt
|
|
1416
|
-
});
|
|
1417
|
-
return diskCached.provider;
|
|
1418
|
-
}
|
|
1419
|
-
for (const [providerId, config] of Object.entries(customProviders)) {
|
|
1420
|
-
if (config.urlPatterns && config.urlPatterns.length > 0) {
|
|
1421
|
-
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1422
|
-
for (const pattern of config.urlPatterns) {
|
|
1423
|
-
const normalizedPattern = pattern.toLowerCase();
|
|
1424
|
-
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1425
|
-
logger.debug("Provider detected via custom URL pattern", { provider: providerId, pattern });
|
|
1426
|
-
cacheProviderDetection(baseUrl, providerId, "url-pattern");
|
|
1427
|
-
return providerId;
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
|
|
1433
|
-
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
|
|
1434
|
-
if (probedProvider) {
|
|
1435
|
-
logger.debug("Provider detected via health probe", { provider: probedProvider });
|
|
1436
|
-
cacheProviderDetection(baseUrl, probedProvider, "health-probe");
|
|
1437
|
-
return probedProvider;
|
|
1438
|
-
}
|
|
1439
|
-
const patternProvider = detectProviderFromUrlPattern(baseUrl, {});
|
|
1440
|
-
logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
|
|
1441
|
-
cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
|
|
1442
|
-
return patternProvider;
|
|
1443
|
-
}
|
|
1444
|
-
function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
1445
|
-
const now = new Date().toISOString();
|
|
1446
|
-
detectionCache.set(baseUrl, {
|
|
1447
|
-
provider,
|
|
1448
|
-
detectedAt: now
|
|
1449
|
-
});
|
|
1450
|
-
writeProviderDetectionCache(baseUrl, {
|
|
1451
|
-
baseUrl,
|
|
1452
|
-
provider,
|
|
1453
|
-
detectedVia,
|
|
1454
|
-
detectedAt: now,
|
|
1455
|
-
ttlSeconds: PROVIDER_DETECTION_TTL_SECONDS
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
1942
|
// src/providers/index.ts
|
|
1460
1943
|
var BUILT_IN_ADAPTERS = {
|
|
1461
1944
|
sub2api: {
|
|
@@ -1465,14 +1948,14 @@ var BUILT_IN_ADAPTERS = {
|
|
|
1465
1948
|
fetch: fetchClaudeRelayService
|
|
1466
1949
|
}
|
|
1467
1950
|
};
|
|
1468
|
-
function getProvider(providerId,
|
|
1951
|
+
function getProvider(providerId, endpointConfigs = {}) {
|
|
1469
1952
|
if (BUILT_IN_ADAPTERS[providerId]) {
|
|
1470
1953
|
return BUILT_IN_ADAPTERS[providerId];
|
|
1471
1954
|
}
|
|
1472
|
-
const
|
|
1473
|
-
if (
|
|
1955
|
+
const endpointConfig = endpointConfigs[providerId];
|
|
1956
|
+
if (endpointConfig) {
|
|
1474
1957
|
return {
|
|
1475
|
-
fetch: (baseUrl, token, config, timeoutMs) =>
|
|
1958
|
+
fetch: (baseUrl, token, config, timeoutMs) => fetchEndpoint(baseUrl, token, config, endpointConfig, timeoutMs)
|
|
1476
1959
|
};
|
|
1477
1960
|
}
|
|
1478
1961
|
return null;
|
|
@@ -1501,10 +1984,30 @@ var ANSI_COLORS = {
|
|
|
1501
1984
|
};
|
|
1502
1985
|
var THEME_COLORS = {
|
|
1503
1986
|
cool: "#56B6C2",
|
|
1504
|
-
comfortable: "#
|
|
1987
|
+
comfortable: "#5EBE8A",
|
|
1505
1988
|
warm: "#C9A84C",
|
|
1506
|
-
hot: "#
|
|
1507
|
-
critical: "#
|
|
1989
|
+
hot: "#D68B45",
|
|
1990
|
+
critical: "#D45A5A",
|
|
1991
|
+
"pastel-cool": "#BAD7F2",
|
|
1992
|
+
"pastel-comfortable": "#BAF2D8",
|
|
1993
|
+
"pastel-medium": "#BAF2BB",
|
|
1994
|
+
"pastel-warm": "#F2E2BA",
|
|
1995
|
+
"pastel-hot": "#F2BAC9",
|
|
1996
|
+
"bright-cool": "#90F1EF",
|
|
1997
|
+
"bright-comfortable": "#7BF1A8",
|
|
1998
|
+
"bright-medium": "#C1FBA4",
|
|
1999
|
+
"bright-warm": "#FFEF9F",
|
|
2000
|
+
"bright-hot": "#FFD6E0",
|
|
2001
|
+
"ocean-cool": "#0081A7",
|
|
2002
|
+
"ocean-comfortable": "#00AFB9",
|
|
2003
|
+
"ocean-medium": "#FDFCDC",
|
|
2004
|
+
"ocean-warm": "#FED9B7",
|
|
2005
|
+
"ocean-hot": "#F07167",
|
|
2006
|
+
"neutral-cool": "#D8E2DC",
|
|
2007
|
+
"neutral-comfortable": "#FFE5D9",
|
|
2008
|
+
"neutral-warm": "#FFCAD4",
|
|
2009
|
+
"neutral-hot": "#F4ACB7",
|
|
2010
|
+
"neutral-critical": "#9D8189"
|
|
1508
2011
|
};
|
|
1509
2012
|
var ANSI_RESET = "\x1B[0m";
|
|
1510
2013
|
var ANSI_DIM = "\x1B[2m";
|
|
@@ -1614,9 +2117,6 @@ function resolveColor(colorName, usagePercent, config) {
|
|
|
1614
2117
|
}
|
|
1615
2118
|
return resolveColorAlias(alias, usagePercent);
|
|
1616
2119
|
}
|
|
1617
|
-
function isTieredEntry(alias) {
|
|
1618
|
-
return "tiers" in alias;
|
|
1619
|
-
}
|
|
1620
2120
|
function resolveTieredColor(entry, usagePercent) {
|
|
1621
2121
|
if (entry.tiers.length === 0)
|
|
1622
2122
|
return null;
|
|
@@ -1633,19 +2133,7 @@ function resolveTieredColor(entry, usagePercent) {
|
|
|
1633
2133
|
function resolveColorAlias(alias, usagePercent) {
|
|
1634
2134
|
if (!alias)
|
|
1635
2135
|
return null;
|
|
1636
|
-
|
|
1637
|
-
return resolveTieredColor(alias, usagePercent);
|
|
1638
|
-
}
|
|
1639
|
-
if (usagePercent === null) {
|
|
1640
|
-
return alias.low;
|
|
1641
|
-
}
|
|
1642
|
-
if (usagePercent < alias.lowThreshold) {
|
|
1643
|
-
return alias.low;
|
|
1644
|
-
} else if (usagePercent < alias.highThreshold) {
|
|
1645
|
-
return alias.medium;
|
|
1646
|
-
} else {
|
|
1647
|
-
return alias.high;
|
|
1648
|
-
}
|
|
2136
|
+
return resolveTieredColor(alias, usagePercent);
|
|
1649
2137
|
}
|
|
1650
2138
|
|
|
1651
2139
|
// src/renderer/transition.ts
|
|
@@ -1696,6 +2184,8 @@ function renderStandaloneError(errorState, provider, message) {
|
|
|
1696
2184
|
return `${warningIcon} Set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`;
|
|
1697
2185
|
case "timeout":
|
|
1698
2186
|
return `${warningIcon} Fetching...`;
|
|
2187
|
+
case "endpoint-config-changed":
|
|
2188
|
+
return `${warningIcon} Endpoint config changed — run: cc-api-statusline --apply-config`;
|
|
1699
2189
|
case "network-error":
|
|
1700
2190
|
case "server-error":
|
|
1701
2191
|
case "parse-error":
|
|
@@ -2481,10 +2971,10 @@ function isComponentId(key) {
|
|
|
2481
2971
|
|
|
2482
2972
|
// src/core/execute-cycle.ts
|
|
2483
2973
|
async function executeCycle(ctx) {
|
|
2484
|
-
const { env, config, configHash, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
2974
|
+
const { env, config, configHash, endpointConfigHash, endpointLock, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
2485
2975
|
if (cachedEntry) {
|
|
2486
2976
|
if (isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
2487
|
-
if (cachedEntry.renderedLine && isCacheRenderedLineUsable(cachedEntry, configHash)) {
|
|
2977
|
+
if (cachedEntry.renderedLine && isCacheRenderedLineUsable(cachedEntry, configHash) && cachedEntry.endpointConfigHash === endpointConfigHash) {
|
|
2488
2978
|
logger.debug("Path A: Fast path (cached renderedLine)", {
|
|
2489
2979
|
cacheAge: `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s`
|
|
2490
2980
|
});
|
|
@@ -2496,13 +2986,34 @@ async function executeCycle(ctx) {
|
|
|
2496
2986
|
}
|
|
2497
2987
|
}
|
|
2498
2988
|
}
|
|
2989
|
+
if (endpointLock && endpointLock.hash !== endpointConfigHash) {
|
|
2990
|
+
logger.debug("Path B2: Endpoint config changed (locked out)", {
|
|
2991
|
+
lockedHash: endpointLock.hash,
|
|
2992
|
+
currentHash: endpointConfigHash
|
|
2993
|
+
});
|
|
2994
|
+
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
2995
|
+
const statusline = renderStatusline(cachedEntry.data, config);
|
|
2996
|
+
return {
|
|
2997
|
+
output: statusline,
|
|
2998
|
+
exitCode: 0,
|
|
2999
|
+
cacheUpdate: null
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
const errorOutput = renderError("endpoint-config-changed", "without-cache");
|
|
3003
|
+
return {
|
|
3004
|
+
output: errorOutput,
|
|
3005
|
+
exitCode: 0,
|
|
3006
|
+
cacheUpdate: null
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
2499
3009
|
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
2500
3010
|
logger.debug("Path B: Re-render (config changed, cache data valid)");
|
|
2501
3011
|
const statusline = renderStatusline(cachedEntry.data, config);
|
|
2502
3012
|
const updatedEntry = {
|
|
2503
3013
|
...cachedEntry,
|
|
2504
3014
|
renderedLine: statusline,
|
|
2505
|
-
configHash
|
|
3015
|
+
configHash,
|
|
3016
|
+
endpointConfigHash
|
|
2506
3017
|
};
|
|
2507
3018
|
return {
|
|
2508
3019
|
output: statusline,
|
|
@@ -2548,6 +3059,7 @@ async function executeCycle(ctx) {
|
|
|
2548
3059
|
data,
|
|
2549
3060
|
renderedLine: statusline,
|
|
2550
3061
|
configHash,
|
|
3062
|
+
endpointConfigHash,
|
|
2551
3063
|
errorState: null
|
|
2552
3064
|
};
|
|
2553
3065
|
return {
|
|
@@ -2582,23 +3094,23 @@ async function executeCycle(ctx) {
|
|
|
2582
3094
|
}
|
|
2583
3095
|
}
|
|
2584
3096
|
// src/services/cache-gc.ts
|
|
2585
|
-
import { readdirSync, statSync, unlinkSync as
|
|
2586
|
-
import { join as
|
|
3097
|
+
import { readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync6, existsSync as existsSync11 } from "fs";
|
|
3098
|
+
import { join as join11 } from "path";
|
|
2587
3099
|
function runCacheGC(cacheDir) {
|
|
2588
3100
|
try {
|
|
2589
|
-
if (!
|
|
3101
|
+
if (!existsSync11(cacheDir)) {
|
|
2590
3102
|
logger.debug("GC: Cache directory does not exist, skipping", { cacheDir });
|
|
2591
3103
|
return;
|
|
2592
3104
|
}
|
|
2593
3105
|
logger.debug("GC: Starting garbage collection", { cacheDir });
|
|
2594
|
-
const files =
|
|
3106
|
+
const files = readdirSync4(cacheDir);
|
|
2595
3107
|
const cacheFiles = [];
|
|
2596
3108
|
const providerDetectFiles = [];
|
|
2597
3109
|
const tmpFiles = [];
|
|
2598
3110
|
for (const file of files) {
|
|
2599
3111
|
try {
|
|
2600
|
-
const filePath =
|
|
2601
|
-
const stats =
|
|
3112
|
+
const filePath = join11(cacheDir, file);
|
|
3113
|
+
const stats = statSync2(filePath);
|
|
2602
3114
|
const mtime = stats.mtimeMs;
|
|
2603
3115
|
if (file.startsWith("cache-") && file.endsWith(".json")) {
|
|
2604
3116
|
cacheFiles.push({ name: file, mtime });
|
|
@@ -2617,7 +3129,7 @@ function runCacheGC(cacheDir) {
|
|
|
2617
3129
|
const age = now - file.mtime;
|
|
2618
3130
|
if (age > GC_MAX_AGE_MS) {
|
|
2619
3131
|
try {
|
|
2620
|
-
|
|
3132
|
+
unlinkSync6(join11(cacheDir, file.name));
|
|
2621
3133
|
deletedCount++;
|
|
2622
3134
|
logger.debug("GC: Deleted old cache file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
|
|
2623
3135
|
} catch (error) {
|
|
@@ -2629,7 +3141,7 @@ function runCacheGC(cacheDir) {
|
|
|
2629
3141
|
const age = now - file.mtime;
|
|
2630
3142
|
if (age > GC_MAX_AGE_MS) {
|
|
2631
3143
|
try {
|
|
2632
|
-
|
|
3144
|
+
unlinkSync6(join11(cacheDir, file.name));
|
|
2633
3145
|
deletedCount++;
|
|
2634
3146
|
logger.debug("GC: Deleted old provider-detect file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
|
|
2635
3147
|
} catch (error) {
|
|
@@ -2641,7 +3153,7 @@ function runCacheGC(cacheDir) {
|
|
|
2641
3153
|
const age = now - file.mtime;
|
|
2642
3154
|
if (age > GC_ORPHAN_TMP_AGE_MS) {
|
|
2643
3155
|
try {
|
|
2644
|
-
|
|
3156
|
+
unlinkSync6(join11(cacheDir, file.name));
|
|
2645
3157
|
deletedCount++;
|
|
2646
3158
|
logger.debug("GC: Deleted orphaned tmp file", { file: file.name, ageMinutes: Math.floor(age / (60 * 1000)) });
|
|
2647
3159
|
} catch (error) {
|
|
@@ -2658,7 +3170,7 @@ function runCacheGC(cacheDir) {
|
|
|
2658
3170
|
const toDelete = remainingCacheFiles.slice(0, remainingCacheFiles.length - GC_MAX_CACHE_FILES);
|
|
2659
3171
|
for (const file of toDelete) {
|
|
2660
3172
|
try {
|
|
2661
|
-
|
|
3173
|
+
unlinkSync6(join11(cacheDir, file.name));
|
|
2662
3174
|
deletedCount++;
|
|
2663
3175
|
logger.debug("GC: Deleted cache file (count limit)", { file: file.name });
|
|
2664
3176
|
} catch (error) {
|
|
@@ -2673,60 +3185,123 @@ function runCacheGC(cacheDir) {
|
|
|
2673
3185
|
}
|
|
2674
3186
|
|
|
2675
3187
|
// src/cli/piped-mode.ts
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3188
|
+
class StatuslineError extends Error {
|
|
3189
|
+
errorType;
|
|
3190
|
+
constructor(errorType) {
|
|
3191
|
+
super(errorType);
|
|
3192
|
+
this.errorType = errorType;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
function readAndValidateEnv() {
|
|
3196
|
+
const env = readCurrentEnv();
|
|
3197
|
+
logger.debug("Environment loaded", {
|
|
3198
|
+
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
3199
|
+
hasToken: !!env.authToken,
|
|
3200
|
+
providerOverride: env.providerOverride,
|
|
3201
|
+
pollIntervalOverride: env.pollIntervalOverride
|
|
3202
|
+
});
|
|
3203
|
+
const envError = validateRequiredEnv(env);
|
|
3204
|
+
if (envError) {
|
|
3205
|
+
throw new StatuslineError("missing-env");
|
|
3206
|
+
}
|
|
3207
|
+
const { baseUrl } = env;
|
|
3208
|
+
if (!baseUrl) {
|
|
3209
|
+
process.exit(1);
|
|
3210
|
+
}
|
|
3211
|
+
return { env, baseUrl };
|
|
3212
|
+
}
|
|
3213
|
+
function ensureDefaultConfigs() {
|
|
3214
|
+
if (needsConfigInit()) {
|
|
3215
|
+
logger.debug("First run detected - initializing default configs");
|
|
3216
|
+
writeDefaultConfigs();
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
function loadConfigWithHash(configPath) {
|
|
3220
|
+
const config = loadConfig(configPath);
|
|
3221
|
+
const resolvedPath = getConfigPath(configPath);
|
|
3222
|
+
const configHash = computeConfigHash(resolvedPath);
|
|
3223
|
+
logger.debug("Config loaded", { configPath: resolvedPath, configHash });
|
|
3224
|
+
return { config, configHash };
|
|
3225
|
+
}
|
|
3226
|
+
function loadEndpointConfigsWithHash() {
|
|
3227
|
+
const endpointConfigs = loadEndpointConfigs();
|
|
3228
|
+
const endpointConfigHash = computeEndpointConfigHash();
|
|
3229
|
+
logger.debug("Endpoint configs loaded", {
|
|
3230
|
+
configCount: Object.keys(endpointConfigs).length,
|
|
3231
|
+
endpointConfigHash
|
|
3232
|
+
});
|
|
3233
|
+
return { endpointConfigs, endpointConfigHash };
|
|
3234
|
+
}
|
|
3235
|
+
function resolveEndpointLock(hash) {
|
|
3236
|
+
const existing = readEndpointLock();
|
|
3237
|
+
if (existing) {
|
|
3238
|
+
logger.debug("Endpoint lock file loaded", {
|
|
3239
|
+
lockedHash: existing.hash,
|
|
3240
|
+
currentHash: hash,
|
|
3241
|
+
locked: existing.hash === hash
|
|
2684
3242
|
});
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3243
|
+
return existing;
|
|
3244
|
+
}
|
|
3245
|
+
logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3246
|
+
writeEndpointLock(hash);
|
|
3247
|
+
return { hash, lockedAt: new Date().toISOString() };
|
|
3248
|
+
}
|
|
3249
|
+
async function resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, timeoutMs) {
|
|
3250
|
+
const probeTimeout = isPiped ? Math.min(1500, Math.max(200, timeoutMs - 200)) : 3000;
|
|
3251
|
+
const providerId = await resolveProvider(baseUrl, env.providerOverride, endpointConfigs, probeTimeout);
|
|
3252
|
+
const provider = getProvider(providerId, endpointConfigs);
|
|
3253
|
+
logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
3254
|
+
if (!provider) {
|
|
3255
|
+
logger.error("Provider not found", { providerId });
|
|
3256
|
+
throw new StatuslineError("provider-unknown");
|
|
3257
|
+
}
|
|
3258
|
+
return { providerId, provider };
|
|
3259
|
+
}
|
|
3260
|
+
function computeTimeoutBudgets(isPiped, config, timeoutMs) {
|
|
3261
|
+
const timeoutBudgetMs = isPiped ? timeoutMs : 1e4;
|
|
3262
|
+
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? 800, timeoutBudgetMs - 100) : 1e4;
|
|
3263
|
+
return { timeoutBudgetMs, fetchTimeoutMs };
|
|
3264
|
+
}
|
|
3265
|
+
async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs) {
|
|
3266
|
+
const { env, baseUrl } = readAndValidateEnv();
|
|
3267
|
+
ensureDefaultConfigs();
|
|
3268
|
+
const { config, configHash } = loadConfigWithHash(args.configPath);
|
|
3269
|
+
const { endpointConfigs, endpointConfigHash } = loadEndpointConfigsWithHash();
|
|
3270
|
+
const endpointLock = resolveEndpointLock(endpointConfigHash);
|
|
3271
|
+
const cachedEntry = readCache(baseUrl);
|
|
3272
|
+
logger.debug("Cache read", {
|
|
3273
|
+
cacheHit: !!cachedEntry,
|
|
3274
|
+
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
3275
|
+
});
|
|
3276
|
+
let providerId;
|
|
3277
|
+
let provider;
|
|
3278
|
+
if (cachedEntry && isCacheValid(cachedEntry, env)) {
|
|
3279
|
+
const cachedProvider = getProvider(cachedEntry.provider, endpointConfigs);
|
|
3280
|
+
if (cachedProvider) {
|
|
3281
|
+
providerId = cachedEntry.provider;
|
|
3282
|
+
provider = cachedProvider;
|
|
3283
|
+
logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3284
|
+
} else {
|
|
3285
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
2709
3286
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
return { ctx, baseUrl };
|
|
2729
|
-
})();
|
|
3287
|
+
} else {
|
|
3288
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3289
|
+
}
|
|
3290
|
+
const { timeoutBudgetMs, fetchTimeoutMs } = computeTimeoutBudgets(isPiped, config, rawTimeoutMs);
|
|
3291
|
+
const ctx = {
|
|
3292
|
+
env,
|
|
3293
|
+
config,
|
|
3294
|
+
configHash,
|
|
3295
|
+
endpointConfigHash,
|
|
3296
|
+
endpointLock,
|
|
3297
|
+
cachedEntry,
|
|
3298
|
+
providerId,
|
|
3299
|
+
provider,
|
|
3300
|
+
timeoutBudgetMs,
|
|
3301
|
+
startTime,
|
|
3302
|
+
fetchTimeoutMs
|
|
3303
|
+
};
|
|
3304
|
+
return { ctx, baseUrl };
|
|
2730
3305
|
}
|
|
2731
3306
|
function formatOutput(output, isPiped) {
|
|
2732
3307
|
let normalizedOutput = output;
|
|
@@ -2748,12 +3323,45 @@ async function executePipedMode(args) {
|
|
|
2748
3323
|
logger.debug("Start time", { startTime });
|
|
2749
3324
|
const isPiped = !process.stdin.isTTY;
|
|
2750
3325
|
logger.debug("Mode detection", { isPiped, once: args.once });
|
|
2751
|
-
const
|
|
3326
|
+
const rawTimeoutMs = Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000);
|
|
3327
|
+
if (isPiped) {
|
|
3328
|
+
const watchdogMs = rawTimeoutMs - 100;
|
|
3329
|
+
setTimeout(() => {
|
|
3330
|
+
logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
|
|
3331
|
+
const fallback = dimText("⟳ Refreshing...");
|
|
3332
|
+
const formatted = formatOutput(fallback, isPiped);
|
|
3333
|
+
process.stdout.write(formatted);
|
|
3334
|
+
process.exit(0);
|
|
3335
|
+
}, watchdogMs).unref();
|
|
3336
|
+
}
|
|
3337
|
+
let ctx;
|
|
3338
|
+
let baseUrl;
|
|
3339
|
+
try {
|
|
3340
|
+
({ ctx, baseUrl } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs));
|
|
3341
|
+
} catch (error) {
|
|
3342
|
+
logger.error("Failed to build execution context", { error: String(error) });
|
|
3343
|
+
const errorType = error instanceof StatuslineError ? error.errorType : "network-error";
|
|
3344
|
+
const errorOutput = renderError(errorType, "without-cache");
|
|
3345
|
+
const formattedOutput2 = formatOutput(errorOutput, isPiped);
|
|
3346
|
+
process.stdout.write(formattedOutput2);
|
|
3347
|
+
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3348
|
+
process.exit(0);
|
|
3349
|
+
}
|
|
2752
3350
|
logger.debug("Execution context prepared", {
|
|
2753
3351
|
timeoutBudgetMs: ctx.timeoutBudgetMs,
|
|
2754
3352
|
fetchTimeoutMs: ctx.fetchTimeoutMs
|
|
2755
3353
|
});
|
|
2756
|
-
|
|
3354
|
+
let result;
|
|
3355
|
+
try {
|
|
3356
|
+
result = await executeCycle(ctx);
|
|
3357
|
+
} catch (error) {
|
|
3358
|
+
logger.error("Execution cycle failed", { error: String(error) });
|
|
3359
|
+
const errorOutput = renderError("network-error", "without-cache");
|
|
3360
|
+
const formattedOutput2 = formatOutput(errorOutput, isPiped);
|
|
3361
|
+
process.stdout.write(formattedOutput2);
|
|
3362
|
+
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3363
|
+
process.exit(0);
|
|
3364
|
+
}
|
|
2757
3365
|
const executionTime = Date.now() - startTime;
|
|
2758
3366
|
logger.debug("Execution completed", {
|
|
2759
3367
|
exitCode: result.exitCode,
|
|
@@ -2776,6 +3384,7 @@ function discardStdin() {
|
|
|
2776
3384
|
if (!process.stdin.isTTY) {
|
|
2777
3385
|
process.stdin.resume();
|
|
2778
3386
|
process.stdin.on("data", () => {});
|
|
3387
|
+
process.stdin.on("error", () => {});
|
|
2779
3388
|
}
|
|
2780
3389
|
}
|
|
2781
3390
|
async function main() {
|
|
@@ -2799,6 +3408,10 @@ async function main() {
|
|
|
2799
3408
|
handleUninstall();
|
|
2800
3409
|
return;
|
|
2801
3410
|
}
|
|
3411
|
+
if (args.applyConfig) {
|
|
3412
|
+
handleApplyConfig();
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
2802
3415
|
if (process.stdin.isTTY && !args.once) {
|
|
2803
3416
|
console.log("Interactive configuration mode coming soon.");
|
|
2804
3417
|
console.log("Use --once for a single fetch, or configure as a Claude Code statusline command.");
|
|
@@ -2806,6 +3419,13 @@ async function main() {
|
|
|
2806
3419
|
}
|
|
2807
3420
|
await executePipedMode(args);
|
|
2808
3421
|
}
|
|
3422
|
+
process.on("SIGTERM", () => {
|
|
3423
|
+
process.exit(0);
|
|
3424
|
+
});
|
|
3425
|
+
process.on("uncaughtException", (error) => {
|
|
3426
|
+
logger.error("Uncaught exception", { error: String(error) });
|
|
3427
|
+
process.exit(0);
|
|
3428
|
+
});
|
|
2809
3429
|
main().catch((error) => {
|
|
2810
3430
|
logger.error("Unhandled error in main", { error: String(error) });
|
|
2811
3431
|
console.error(`Fatal error: ${error}`);
|