cc-api-statusline 0.1.3 → 0.1.9

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