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.
@@ -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.1",
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/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
- }
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: 30 },
415
- { color: "comfortable", maxPercent: 65 },
416
- { color: "warm", maxPercent: 80 },
417
- { color: "hot", maxPercent: 90 },
418
- { color: "critical", maxPercent: 100 }
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
- low: "cyan",
423
- medium: "blue",
424
- high: "magenta",
425
- lowThreshold: 50,
426
- highThreshold: 80
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 = 1;
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/cache.ts
511
- function getCacheDir() {
512
- const override = process.env["CC_API_STATUSLINE_CACHE_DIR"];
513
- if (override) {
514
- return override;
515
- }
516
- return join2(homedir2(), ".claude", "cc-api-statusline");
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 ensureCacheDir() {
519
- const dir = getCacheDir();
520
- ensureDir(dir);
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 getCachePath(baseUrl) {
523
- const hash = shortHash(baseUrl, 12);
524
- return join2(getCacheDir(), `cache-${hash}.json`);
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
- function readCache(baseUrl) {
527
- const path = getCachePath(baseUrl);
528
- if (!existsSync5(path)) {
529
- return null;
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
- try {
532
- const content = readFileSync3(path, "utf-8");
533
- const data = JSON.parse(content);
534
- if (!isCacheEntry(data)) {
535
- console.warn(`Invalid cache structure at ${path}`);
536
- return null;
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
- function writeCache(baseUrl, entry) {
545
- const path = getCachePath(baseUrl);
546
- try {
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
- function isCacheValid(entry, currentEnv) {
555
- const fetchedAt = new Date(entry.fetchedAt).getTime();
556
- const now = Date.now();
557
- const age = now - fetchedAt;
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
- if (entry.baseUrl !== currentEnv.baseUrl) {
563
- return false;
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
- if (entry.version !== CACHE_VERSION) {
566
- return false;
671
+ debug(message, data) {
672
+ this.write("debug", message, data);
567
673
  }
568
- if (entry.tokenHash !== currentEnv.tokenHash) {
569
- return false;
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
- function isCacheProviderValid(entry, currentProvider) {
574
- return entry.provider === currentProvider;
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 isCacheRenderedLineUsable(entry, currentConfigHash) {
577
- return entry.configHash === currentConfigHash;
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 computeConfigHash(configPath) {
580
- if (!existsSync5(configPath)) {
581
- return sha256("").slice(0, 12);
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
- try {
584
- const bytes = readFileSync3(configPath);
585
- return shortHash(bytes.toString("utf-8"), 12);
586
- } catch (error) {
587
- console.warn(`Failed to read config for hash: ${error}`);
588
- return sha256("").slice(0, 12);
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
- var DEFAULT_POLL_INTERVAL_SECONDS = 30;
592
- function getEffectivePollInterval(config, envOverride) {
593
- if (envOverride !== null) {
594
- return Math.max(5, envOverride);
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 fromConfig = config.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS;
597
- return Math.max(5, fromConfig);
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 getProviderDetectionCachePath(baseUrl) {
600
- const hash = shortHash(baseUrl, 12);
601
- return join2(getCacheDir(), `provider-detect-${hash}.json`);
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
- function readProviderDetectionCache(baseUrl) {
604
- const path = getProviderDetectionCachePath(baseUrl);
605
- if (!existsSync5(path)) {
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 = readFileSync3(path, "utf-8");
907
+ const content = readFileSync4(lockPath, "utf-8");
610
908
  const data = JSON.parse(content);
611
- if (!isProviderDetectionCacheEntry(data)) {
612
- console.warn(`Invalid provider detection cache structure at ${path}`);
613
- return null;
614
- }
615
- const detectedAt = new Date(data.detectedAt).getTime();
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 data;
626
- } catch (error) {
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 writeProviderDetectionCache(baseUrl, entry) {
632
- const path = getProviderDetectionCachePath(baseUrl);
633
- try {
634
- ensureCacheDir();
635
- const content = JSON.stringify(entry, null, 2);
636
- atomicWriteFile(path, content);
637
- } catch (error) {
638
- console.warn(`Failed to write provider detection cache to ${path}: ${error}`);
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
- import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
644
- import { join as join3 } from "path";
645
- import { homedir as homedir3 } from "os";
646
- function getConfigDir() {
647
- return join3(homedir3(), ".claude", "cc-api-statusline");
648
- }
649
- function getConfigPath(customPath) {
650
- if (customPath) {
651
- return customPath;
652
- }
653
- return join3(getConfigDir(), "config.json");
654
- }
655
- function deepMerge(target, source) {
656
- const result = { ...target };
657
- for (const key in source) {
658
- const sourceValue = source[key];
659
- const targetValue = result[key];
660
- if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
661
- result[key] = deepMerge(targetValue, sourceValue);
662
- } else if (sourceValue !== undefined) {
663
- result[key] = sourceValue;
664
- }
665
- }
666
- return result;
667
- }
668
- function validateConfig(config) {
669
- let maxWidth = config.display.maxWidth;
670
- let pollIntervalSeconds = config.pollIntervalSeconds;
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
- if (pollIntervalSeconds !== undefined && pollIntervalSeconds < 5) {
681
- console.warn("Warning: pollIntervalSeconds < 5, clamping to 5");
682
- pollIntervalSeconds = 5;
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
- if (pipedRequestTimeoutMs !== undefined && pipedRequestTimeoutMs < 100) {
685
- console.warn("Warning: pipedRequestTimeoutMs < 100, clamping to 100");
686
- pipedRequestTimeoutMs = 100;
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
- return {
689
- ...config,
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 loadConfig(configPath) {
699
- const path = getConfigPath(configPath);
700
- if (!existsSync6(path)) {
701
- return DEFAULT_CONFIG;
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/services/user-agent.ts
820
- import { execSync as execSync2 } from "child_process";
821
- import { join as join5 } from "path";
822
- import { homedir as homedir5 } from "os";
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/logger.ts
825
- import { appendFileSync } from "fs";
826
- import { join as join4, dirname as dirname2 } from "path";
827
- import { homedir as homedir4 } from "os";
828
- class Logger {
829
- enabled;
830
- logPath;
831
- constructor() {
832
- this.enabled = !!(process.env["DEBUG"] || process.env["CC_STATUSLINE_DEBUG"]);
833
- const logDir = process.env["CC_API_STATUSLINE_LOG_DIR"] || join4(homedir4(), ".claude", "cc-api-statusline");
834
- this.logPath = join4(logDir, "debug.log");
835
- if (this.enabled) {
836
- this.ensureLogDir();
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
- ensureLogDir() {
840
- try {
841
- const dir = dirname2(this.logPath);
842
- ensureDir(dir);
843
- } catch {
844
- this.enabled = false;
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
- formatLocalTimestamp() {
848
- const d = new Date;
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
- format(level, message, data) {
853
- const timestamp = this.formatLocalTimestamp();
854
- const dataStr = data ? ` ${JSON.stringify(data)}` : "";
855
- return `[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}
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
- write(level, message, data) {
859
- if (!this.enabled) {
860
- return;
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
- debug(message, data) {
868
- this.write("debug", message, data);
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
- info(message, data) {
871
- this.write("info", message, data);
1460
+ if (maxWidth > 100) {
1461
+ console.warn("Warning: display.maxWidth > 100, clamping to 100");
1462
+ maxWidth = 100;
872
1463
  }
873
- warn(message, data) {
874
- this.write("warn", message, data);
1464
+ if (pollIntervalSeconds !== undefined && pollIntervalSeconds < 5) {
1465
+ console.warn("Warning: pollIntervalSeconds < 5, clamping to 5");
1466
+ pollIntervalSeconds = 5;
875
1467
  }
876
- error(message, data) {
877
- this.write("error", message, data);
1468
+ if (pipedRequestTimeoutMs !== undefined && pipedRequestTimeoutMs < 100) {
1469
+ console.warn("Warning: pipedRequestTimeoutMs < 100, clamping to 100");
1470
+ pipedRequestTimeoutMs = 100;
878
1471
  }
879
- isEnabled() {
880
- return this.enabled;
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
- getLogPath() {
883
- return this.logPath;
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 = join5(homedir5(), ".claude", "bin", "claude");
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/custom-mapping.ts
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, providerConfig) {
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, providerConfig.displayName ?? providerConfig.id);
1243
- const base = createEmptyNormalizedUsage(providerConfig.id, billingMode, planName);
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/custom.ts
1307
- function validateCustomProvider(providerConfig) {
1308
- if (!providerConfig.id)
1309
- return "Custom provider missing required field: id";
1310
- if (!providerConfig.endpoint)
1311
- return "Custom provider missing required field: endpoint";
1312
- if (!providerConfig.method)
1313
- return "Custom provider missing required field: method";
1314
- if (!providerConfig.auth)
1315
- return "Custom provider missing required field: auth";
1316
- if (!providerConfig.responseMapping)
1317
- return "Custom provider missing required field: responseMapping";
1318
- if (!providerConfig.endpoint.startsWith("/")) {
1319
- return "Custom provider endpoint must start with /";
1320
- }
1321
- if (!providerConfig.responseMapping.billingMode) {
1322
- return "Custom provider responseMapping must include billingMode";
1323
- }
1324
- if (providerConfig.auth.type === "header" && !providerConfig.auth.header) {
1325
- return 'Custom provider auth.type="header" requires auth.header';
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 fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
1336
- const validationError = validateCustomProvider(providerConfig);
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 custom provider providerConfig: ${validationError}`);
1899
+ throw new Error(`Invalid endpoint config: ${validationError}`);
1339
1900
  }
1340
- const url = `${baseUrl}${providerConfig.endpoint}`;
1901
+ const url = `${baseUrl}${endpointConfig.endpoint.path}`;
1341
1902
  const headers = {
1342
1903
  Accept: "application/json"
1343
1904
  };
1344
- if (providerConfig.contentType) {
1345
- headers["Content-Type"] = providerConfig.contentType;
1905
+ if (endpointConfig.endpoint.contentType) {
1906
+ headers["Content-Type"] = endpointConfig.endpoint.contentType;
1346
1907
  }
1347
- if (providerConfig.auth.type === "header" && providerConfig.auth.header) {
1348
- const prefix = providerConfig.auth.prefix ?? "";
1349
- headers[providerConfig.auth.header] = `${prefix}${token}`;
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 (providerConfig.method === "POST") {
1353
- if (providerConfig.auth.type === "body" && providerConfig.auth.bodyField) {
1354
- const bodyObj = { ...providerConfig.requestBody };
1355
- bodyObj[providerConfig.auth.bodyField] = token;
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 (providerConfig.requestBody) {
1358
- body = JSON.stringify(providerConfig.requestBody);
1921
+ } else if (endpointConfig.requestBody) {
1922
+ body = JSON.stringify(endpointConfig.requestBody);
1359
1923
  }
1360
1924
  }
1361
- const providerUA = providerConfig.spoofClaudeCodeUA;
1925
+ const endpointUA = endpointConfig.spoofClaudeCodeUA;
1362
1926
  const globalUA = appConfig.spoofClaudeCodeUA;
1363
- const effectiveUA = providerUA !== undefined ? providerUA : globalUA;
1927
+ const effectiveUA = endpointUA !== undefined ? endpointUA : globalUA;
1364
1928
  const resolvedUA = resolveUserAgent(effectiveUA);
1365
1929
  if (resolvedUA) {
1366
- logger.debug(`Using User-Agent for ${providerConfig.id}: ${resolvedUA}`);
1930
+ logger.debug(`Using User-Agent for ${endpointConfig.provider}: ${resolvedUA}`);
1367
1931
  }
1368
1932
  const responseText = await secureFetch(url, {
1369
- method: providerConfig.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, providerConfig.responseMapping, providerConfig);
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, customProviders = {}) {
1951
+ function getProvider(providerId, endpointConfigs = {}) {
1469
1952
  if (BUILT_IN_ADAPTERS[providerId]) {
1470
1953
  return BUILT_IN_ADAPTERS[providerId];
1471
1954
  }
1472
- const customConfig = customProviders[providerId];
1473
- if (customConfig) {
1955
+ const endpointConfig = endpointConfigs[providerId];
1956
+ if (endpointConfig) {
1474
1957
  return {
1475
- fetch: (baseUrl, token, config, timeoutMs) => fetchCustom(baseUrl, token, config, customConfig, 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: "#6BAF8D",
1987
+ comfortable: "#5EBE8A",
1505
1988
  warm: "#C9A84C",
1506
- hot: "#CB7E55",
1507
- critical: "#C96B6B"
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
- if (isTieredEntry(alias)) {
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 unlinkSync3, existsSync as existsSync7 } from "fs";
2586
- import { join as join6 } from "path";
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 (!existsSync7(cacheDir)) {
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 = readdirSync(cacheDir);
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 = join6(cacheDir, file);
2601
- const stats = statSync(filePath);
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
- unlinkSync3(join6(cacheDir, file.name));
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
- unlinkSync3(join6(cacheDir, file.name));
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
- unlinkSync3(join6(cacheDir, file.name));
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
- unlinkSync3(join6(cacheDir, file.name));
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
- function buildExecutionContext(args, isPiped, startTime) {
2677
- return (async () => {
2678
- const env = readCurrentEnv();
2679
- logger.debug("Environment loaded", {
2680
- baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
2681
- hasToken: !!env.authToken,
2682
- providerOverride: env.providerOverride,
2683
- pollIntervalOverride: env.pollIntervalOverride
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
- const envError = validateRequiredEnv(env);
2686
- if (envError) {
2687
- const errorOutput = renderError("missing-env", "without-cache");
2688
- process.stdout.write(errorOutput);
2689
- process.exit(0);
2690
- }
2691
- const baseUrl = env.baseUrl;
2692
- const authToken = env.authToken;
2693
- if (!baseUrl || !authToken) {
2694
- process.exit(1);
2695
- }
2696
- const config = loadConfig(args.configPath);
2697
- const configPath = getConfigPath(args.configPath);
2698
- const configHash = computeConfigHash(configPath);
2699
- logger.debug("Config loaded", { configPath, configHash });
2700
- const probeTimeout = isPiped ? Math.min(1500, Math.max(200, Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) - 200)) : 3000;
2701
- const providerId = await resolveProvider(baseUrl, env.providerOverride, config.customProviders ?? {}, probeTimeout);
2702
- const provider = getProvider(providerId, config.customProviders ?? {});
2703
- logger.debug("Provider resolved", { providerId, probeTimeout });
2704
- if (!provider) {
2705
- logger.error("Provider not found", { providerId });
2706
- const errorOutput = renderError("provider-unknown", "without-cache");
2707
- process.stdout.write(errorOutput);
2708
- process.exit(0);
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
- const cachedEntry = readCache(baseUrl);
2711
- logger.debug("Cache read", {
2712
- cacheHit: !!cachedEntry,
2713
- cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
2714
- });
2715
- const timeoutBudgetMs = isPiped ? Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) : 1e4;
2716
- const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? 800, timeoutBudgetMs - 100) : 1e4;
2717
- const ctx = {
2718
- env,
2719
- config,
2720
- configHash,
2721
- cachedEntry,
2722
- providerId,
2723
- provider,
2724
- timeoutBudgetMs,
2725
- startTime,
2726
- fetchTimeoutMs
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 { ctx, baseUrl } = await buildExecutionContext(args, isPiped, startTime);
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
- const result = await executeCycle(ctx);
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}`);