cc-api-statusline 0.1.4 → 0.1.9

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