cc-api-statusline 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "cc-api-statusline",
7
- version: "0.2.2",
7
+ version: "1.0.0",
8
8
  description: "Claude Code statusline tool that polls API usage from third-party proxy backends",
9
9
  type: "module",
10
10
  bin: {
@@ -138,13 +138,13 @@ function showVersion() {
138
138
  console.log(`cc-api-statusline v${package_default.version}`);
139
139
  }
140
140
  // src/services/settings.ts
141
- import { readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
141
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
142
142
  import { execSync } from "child_process";
143
143
 
144
144
  // src/services/env.ts
145
- import { readFileSync, existsSync } from "fs";
146
- import { join } from "path";
147
- import { homedir } from "os";
145
+ import { readFileSync } from "fs";
146
+ import { join as join4 } from "path";
147
+ import { homedir as homedir2 } from "os";
148
148
 
149
149
  // src/services/hash.ts
150
150
  function sha256(input) {
@@ -161,21 +161,193 @@ function shortHash(input, length = 12) {
161
161
  return fullHash.slice(0, length);
162
162
  }
163
163
 
164
+ // src/services/logger.ts
165
+ import { appendFileSync } from "fs";
166
+ import { join as join3, dirname as dirname2 } from "path";
167
+
168
+ // src/services/ensure-dir.ts
169
+ import { mkdirSync } from "fs";
170
+ function ensureDir(dirPath) {
171
+ mkdirSync(dirPath, { recursive: true, mode: 448 });
172
+ }
173
+
174
+ // src/services/log-rotator.ts
175
+ import { statSync, renameSync, readdirSync, unlinkSync } from "fs";
176
+ import { spawn } from "child_process";
177
+ import { dirname, join } from "path";
178
+
179
+ // src/core/constants.ts
180
+ var DEFAULT_FETCH_TIMEOUT_MS = 5000;
181
+ var EXIT_BUFFER_MS = 50;
182
+ var STALENESS_THRESHOLD_MINUTES = 5;
183
+ var VERY_STALE_THRESHOLD_MINUTES = 30;
184
+ var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
185
+ var GC_MAX_CACHE_FILES = 20;
186
+ var GC_ORPHAN_TMP_AGE_MS = 60 * 60 * 1000;
187
+ var LOG_ROTATION_PROBABILITY = 0.05;
188
+ var LOG_MAX_SIZE_BYTES = 512 * 1024;
189
+ var LOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
190
+ var LOG_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
191
+
192
+ // src/services/log-rotator.ts
193
+ var ARCHIVE_LOG_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log$/;
194
+ var ARCHIVE_GZ_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log\.gz$/;
195
+ function archiveName(logPath, now = new Date) {
196
+ const pad = (n) => n.toString().padStart(2, "0");
197
+ const y = now.getFullYear();
198
+ const mo = pad(now.getMonth() + 1);
199
+ const d = pad(now.getDate());
200
+ const h = pad(now.getHours());
201
+ const min = pad(now.getMinutes());
202
+ return join(dirname(logPath), `debug.${y}-${mo}-${d}T${h}-${min}.log`);
203
+ }
204
+ function spawnGzip(filePath) {
205
+ try {
206
+ const child = spawn("gzip", ["-f", filePath], {
207
+ detached: true,
208
+ stdio: "ignore"
209
+ });
210
+ child.unref();
211
+ } catch {}
212
+ }
213
+ function runCleanup(logDir, excludePath) {
214
+ try {
215
+ const files = readdirSync(logDir);
216
+ const now = Date.now();
217
+ for (const name of files) {
218
+ const filePath = join(logDir, name);
219
+ if (filePath === excludePath)
220
+ continue;
221
+ if (ARCHIVE_LOG_RE.test(name)) {
222
+ const s = statSync(filePath, { throwIfNoEntry: false });
223
+ if (s && now - s.mtimeMs >= LOG_MAX_AGE_MS) {
224
+ spawnGzip(filePath);
225
+ }
226
+ continue;
227
+ }
228
+ if (ARCHIVE_GZ_RE.test(name)) {
229
+ const s = statSync(filePath, { throwIfNoEntry: false });
230
+ if (s && now - s.mtimeMs >= LOG_RETENTION_MS) {
231
+ try {
232
+ unlinkSync(filePath);
233
+ } catch {}
234
+ }
235
+ }
236
+ }
237
+ } catch {}
238
+ }
239
+ function maybeRotateLogs(logPath) {
240
+ if (Math.random() > LOG_ROTATION_PROBABILITY)
241
+ return;
242
+ const logDir = dirname(logPath);
243
+ const stat = statSync(logPath, { throwIfNoEntry: false });
244
+ let rotatedArchive = null;
245
+ if (stat) {
246
+ const age = Date.now() - stat.mtimeMs;
247
+ const archive = archiveName(logPath);
248
+ try {
249
+ if (age >= LOG_MAX_AGE_MS) {
250
+ renameSync(logPath, archive);
251
+ spawnGzip(archive);
252
+ rotatedArchive = archive;
253
+ } else if (stat.size >= LOG_MAX_SIZE_BYTES) {
254
+ renameSync(logPath, archive);
255
+ rotatedArchive = archive;
256
+ }
257
+ } catch {}
258
+ }
259
+ runCleanup(logDir, rotatedArchive);
260
+ }
261
+
262
+ // src/services/paths.ts
263
+ import { homedir } from "node:os";
264
+ import { join as join2 } from "node:path";
265
+ function getConfigDir() {
266
+ return join2(homedir(), ".claude", "cc-api-statusline");
267
+ }
268
+
269
+ // src/services/logger.ts
270
+ class Logger {
271
+ enabled;
272
+ logPath;
273
+ constructor() {
274
+ this.enabled = !!(process.env["DEBUG"] || process.env["CC_STATUSLINE_DEBUG"]);
275
+ const logDir = process.env["CC_API_STATUSLINE_LOG_DIR"] || getConfigDir();
276
+ this.logPath = join3(logDir, "debug.log");
277
+ if (this.enabled) {
278
+ this.ensureLogDir();
279
+ maybeRotateLogs(this.logPath);
280
+ }
281
+ }
282
+ ensureLogDir() {
283
+ try {
284
+ const dir = dirname2(this.logPath);
285
+ ensureDir(dir);
286
+ } catch {
287
+ this.enabled = false;
288
+ }
289
+ }
290
+ formatLocalTimestamp() {
291
+ const d = new Date;
292
+ const pad = (n, len = 2) => n.toString().padStart(len, "0");
293
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
294
+ }
295
+ format(level, message, data) {
296
+ const timestamp = this.formatLocalTimestamp();
297
+ const dataStr = data ? ` ${JSON.stringify(data)}` : "";
298
+ return `[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}
299
+ `;
300
+ }
301
+ write(level, message, data) {
302
+ if (!this.enabled) {
303
+ return;
304
+ }
305
+ try {
306
+ const entry = this.format(level, message, data);
307
+ appendFileSync(this.logPath, entry, { encoding: "utf-8" });
308
+ } catch {}
309
+ }
310
+ debug(message, data) {
311
+ this.write("debug", message, data);
312
+ }
313
+ info(message, data) {
314
+ this.write("info", message, data);
315
+ }
316
+ warn(message, data) {
317
+ this.write("warn", message, data);
318
+ }
319
+ error(message, data) {
320
+ this.write("error", message, data);
321
+ }
322
+ isEnabled() {
323
+ return this.enabled;
324
+ }
325
+ getLogPath() {
326
+ return this.logPath;
327
+ }
328
+ }
329
+ var logger = new Logger;
330
+
164
331
  // src/services/env.ts
165
332
  function getSettingsJsonPath() {
166
333
  const configDir = process.env["CLAUDE_CONFIG_DIR"];
167
334
  if (configDir) {
168
- return join(configDir, "settings.json");
335
+ return join4(configDir, "settings.json");
169
336
  }
170
- return join(homedir(), ".claude", "settings.json");
337
+ return join4(homedir2(), ".claude", "settings.json");
171
338
  }
172
339
  function readSettingsJsonEnv() {
173
340
  const settingsPath = getSettingsJsonPath();
174
- if (!existsSync(settingsPath)) {
175
- return {};
341
+ let content;
342
+ try {
343
+ content = readFileSync(settingsPath, "utf-8");
344
+ } catch (err) {
345
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
346
+ return {};
347
+ }
348
+ throw err;
176
349
  }
177
350
  try {
178
- const content = readFileSync(settingsPath, "utf-8");
179
351
  const settings = JSON.parse(content);
180
352
  if (settings["env"] && typeof settings["env"] === "object") {
181
353
  const env = settings["env"];
@@ -190,8 +362,8 @@ function readSettingsJsonEnv() {
190
362
  return result;
191
363
  }
192
364
  return {};
193
- } catch (error) {
194
- console.warn(`Warning: Could not read settings.json: ${error}`);
365
+ } catch (err) {
366
+ logger.warn(`Could not read settings.json: ${err}`);
195
367
  return {};
196
368
  }
197
369
  }
@@ -231,25 +403,15 @@ function validateRequiredEnv(env) {
231
403
  }
232
404
 
233
405
  // src/services/atomic-write.ts
234
- import { writeFileSync, renameSync, unlinkSync, existsSync as existsSync3, chmodSync } from "fs";
235
- import { dirname } from "path";
236
-
237
- // src/services/ensure-dir.ts
238
- import { mkdirSync, existsSync as existsSync2 } from "fs";
239
- function ensureDir(dirPath) {
240
- if (!existsSync2(dirPath)) {
241
- mkdirSync(dirPath, { recursive: true, mode: 448 });
242
- }
243
- }
244
-
245
- // src/services/atomic-write.ts
406
+ import { writeFileSync, renameSync as renameSync2, unlinkSync as unlinkSync2, existsSync, chmodSync } from "fs";
407
+ import { dirname as dirname3 } from "path";
246
408
  function atomicWriteFile(filePath, content, opts = {}) {
247
409
  const { mode = 384, ensureParentDir: ensureParent = false, appendNewline = false } = opts;
248
410
  const nonce = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
249
411
  const tmpPath = `${filePath}.${nonce}.tmp`;
250
412
  try {
251
413
  if (ensureParent) {
252
- const dir = dirname(filePath);
414
+ const dir = dirname3(filePath);
253
415
  ensureDir(dir);
254
416
  }
255
417
  const finalContent = appendNewline ? `${content}
@@ -258,11 +420,11 @@ function atomicWriteFile(filePath, content, opts = {}) {
258
420
  try {
259
421
  chmodSync(tmpPath, mode);
260
422
  } catch {}
261
- renameSync(tmpPath, filePath);
423
+ renameSync2(tmpPath, filePath);
262
424
  } catch (error) {
263
425
  try {
264
- if (existsSync3(tmpPath)) {
265
- unlinkSync(tmpPath);
426
+ if (existsSync(tmpPath)) {
427
+ unlinkSync2(tmpPath);
266
428
  }
267
429
  } catch {}
268
430
  throw new Error(`Failed to write file atomically: ${error}`);
@@ -272,14 +434,14 @@ function atomicWriteFile(filePath, content, opts = {}) {
272
434
  // src/services/settings.ts
273
435
  function loadClaudeSettings() {
274
436
  const path = getSettingsJsonPath();
275
- if (!existsSync4(path)) {
437
+ if (!existsSync2(path)) {
276
438
  return {};
277
439
  }
278
440
  try {
279
441
  const content = readFileSync2(path, "utf-8");
280
442
  return JSON.parse(content);
281
443
  } catch (error) {
282
- console.warn(`Failed to read settings from ${path}: ${error}`);
444
+ logger.warn(`Failed to read settings from ${path}: ${error}`);
283
445
  return {};
284
446
  }
285
447
  }
@@ -326,9 +488,8 @@ function uninstallStatusLine() {
326
488
  }
327
489
 
328
490
  // src/services/config-defaults.ts
329
- import { join as join6 } from "path";
330
- import { homedir as homedir5 } from "os";
331
- import { existsSync as existsSync7 } from "fs";
491
+ import { join as join8 } from "path";
492
+ import { existsSync as existsSync4 } from "fs";
332
493
 
333
494
  // src/types/normalized-usage.ts
334
495
  function createEmptyNormalizedUsage(provider, billingMode, planName) {
@@ -361,6 +522,14 @@ function computeSoonestReset(usage) {
361
522
  return sorted[0] ?? null;
362
523
  }
363
524
  // src/types/config.ts
525
+ var DEFAULT_DIVIDER_CONFIG = { text: "|", margin: 1, color: "#555753" };
526
+ var DEFAULT_TIER_THRESHOLDS = [37.5, 62.5, 75, 87.5, 100];
527
+ function buildTiers(colors, thresholds = DEFAULT_TIER_THRESHOLDS) {
528
+ if (colors.length !== thresholds.length) {
529
+ throw new Error(`buildTiers: colors.length (${colors.length}) must equal thresholds.length (${thresholds.length})`);
530
+ }
531
+ return colors.map((color, i) => ({ color, maxPercent: thresholds[i] }));
532
+ }
364
533
  var DEFAULT_CONFIG = {
365
534
  display: {
366
535
  layout: "standard",
@@ -368,7 +537,7 @@ var DEFAULT_CONFIG = {
368
537
  progressStyle: "icon",
369
538
  barSize: "medium",
370
539
  barStyle: "block",
371
- separator: " | ",
540
+ divider: DEFAULT_DIVIDER_CONFIG,
372
541
  maxWidth: 100,
373
542
  clockFormat: "24h",
374
543
  colorMode: "auto",
@@ -381,73 +550,16 @@ var DEFAULT_CONFIG = {
381
550
  balance: true,
382
551
  tokens: false,
383
552
  rateLimit: false,
384
- plan: false,
385
- divider: true
553
+ plan: false
386
554
  },
387
555
  colors: {
388
- auto: {
389
- tiers: [
390
- { color: "cool", maxPercent: 37.5 },
391
- { color: "comfortable", maxPercent: 62.5 },
392
- { color: "warm", maxPercent: 75 },
393
- { color: "hot", maxPercent: 85 },
394
- { color: "critical", maxPercent: 92.5 }
395
- ]
396
- },
397
- vibrant: {
398
- tiers: [
399
- { color: "#00D9FF", maxPercent: 37.5 },
400
- { color: "#4ADE80", maxPercent: 62.5 },
401
- { color: "#FDE047", maxPercent: 75 },
402
- { color: "#FB923C", maxPercent: 85 },
403
- { color: "#F87171", maxPercent: 92.5 }
404
- ]
405
- },
406
- pastel: {
407
- tiers: [
408
- { color: "pastel-cool", maxPercent: 37.5 },
409
- { color: "pastel-comfortable", maxPercent: 62.5 },
410
- { color: "pastel-medium", maxPercent: 75 },
411
- { color: "pastel-warm", maxPercent: 85 },
412
- { color: "pastel-hot", maxPercent: 92.5 }
413
- ]
414
- },
415
- bright: {
416
- tiers: [
417
- { color: "bright-cool", maxPercent: 37.5 },
418
- { color: "bright-comfortable", maxPercent: 62.5 },
419
- { color: "bright-medium", maxPercent: 75 },
420
- { color: "bright-warm", maxPercent: 85 },
421
- { color: "bright-hot", maxPercent: 92.5 }
422
- ]
423
- },
424
- ocean: {
425
- tiers: [
426
- { color: "ocean-cool", maxPercent: 37.5 },
427
- { color: "ocean-comfortable", maxPercent: 62.5 },
428
- { color: "ocean-medium", maxPercent: 75 },
429
- { color: "ocean-warm", maxPercent: 85 },
430
- { color: "ocean-hot", maxPercent: 92.5 }
431
- ]
432
- },
433
- neutral: {
434
- tiers: [
435
- { color: "neutral-cool", maxPercent: 37.5 },
436
- { color: "neutral-comfortable", maxPercent: 62.5 },
437
- { color: "neutral-warm", maxPercent: 75 },
438
- { color: "neutral-hot", maxPercent: 85 },
439
- { color: "neutral-critical", maxPercent: 92.5 }
440
- ]
441
- },
442
- chill: {
443
- tiers: [
444
- { color: "cyan", maxPercent: 37.5 },
445
- { color: "cyan", maxPercent: 62.5 },
446
- { color: "blue", maxPercent: 75 },
447
- { color: "blue", maxPercent: 87.5 },
448
- { color: "magenta", maxPercent: 92.5 }
449
- ]
450
- }
556
+ auto: { tiers: buildTiers(["cool", "comfortable", "warm", "hot", "critical"]) },
557
+ vibrant: { tiers: buildTiers(["#00D9FF", "#4ADE80", "#FDE047", "#FB923C", "#F87171"]) },
558
+ pastel: { tiers: buildTiers(["pastel-cool", "pastel-comfortable", "pastel-medium", "pastel-warm", "pastel-hot"]) },
559
+ bright: { tiers: buildTiers(["bright-cool", "bright-comfortable", "bright-medium", "bright-warm", "bright-hot"]) },
560
+ ocean: { tiers: buildTiers(["ocean-cool", "ocean-comfortable", "ocean-medium", "ocean-warm", "ocean-hot"]) },
561
+ neutral: { tiers: buildTiers(["neutral-cool", "neutral-comfortable", "neutral-warm", "neutral-hot", "neutral-critical"]) },
562
+ chill: { tiers: buildTiers(["cyan", "cyan", "blue", "blue", "magenta"]) }
451
563
  },
452
564
  pollIntervalSeconds: 30,
453
565
  pipedRequestTimeoutMs: 800
@@ -478,226 +590,68 @@ var COMPONENT_SHORT_LABELS = {
478
590
  rateLimit: "R",
479
591
  plan: "P"
480
592
  };
481
- var COMPONENT_FULL_LABELS = {
482
- daily: "Daily",
483
- weekly: "Weekly",
484
- monthly: "Monthly",
485
- balance: "Balance",
486
- tokens: "Tokens",
487
- rateLimit: "Rate",
488
- plan: "Plan"
489
- };
490
- var COMPONENT_EMOJI_LABELS = {
491
- daily: "\uD83D\uDCC5",
492
- weekly: "\uD83D\uDCC6",
493
- monthly: "\uD83D\uDDD3️",
494
- balance: "\uD83D\uDCB0",
495
- tokens: "\uD83D\uDD22",
496
- rateLimit: "⚡",
497
- plan: "\uD83D\uDCCB"
498
- };
499
- var COMPONENT_NERD_LABELS = {
500
- daily: "",
501
- weekly: "",
502
- monthly: "",
503
- balance: "",
504
- tokens: "",
505
- rateLimit: "",
506
- plan: ""
507
- };
508
- var DEFAULT_COMPONENT_ORDER = [
509
- "daily",
510
- "weekly",
511
- "monthly",
512
- "balance",
513
- "tokens",
514
- "rateLimit",
515
- "plan"
516
- ];
517
- // src/types/cache.ts
518
- var CACHE_VERSION = 2;
519
- function isCacheEntry(value) {
520
- if (typeof value !== "object" || value === null)
521
- return false;
522
- const c = value;
523
- return typeof c["version"] === "number" && typeof c["provider"] === "string" && typeof c["baseUrl"] === "string" && typeof c["tokenHash"] === "string" && typeof c["configHash"] === "string" && typeof c["endpointConfigHash"] === "string" && typeof c["data"] === "object" && c["data"] !== null && typeof c["renderedLine"] === "string" && typeof c["fetchedAt"] === "string" && typeof c["ttlSeconds"] === "number" && (c["errorState"] === null || typeof c["errorState"] === "object");
524
- }
525
- var PROVIDER_DETECTION_TTL_SECONDS = 86400;
526
- function isProviderDetectionCacheEntry(value) {
527
- if (typeof value !== "object" || value === null)
528
- return false;
529
- const c = value;
530
- return typeof c["baseUrl"] === "string" && typeof c["provider"] === "string" && (c["detectedVia"] === "health-probe" || c["detectedVia"] === "url-pattern" || c["detectedVia"] === "override") && typeof c["detectedAt"] === "string" && typeof c["ttlSeconds"] === "number";
531
- }
532
- // src/services/endpoint-config.ts
533
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
534
- import { join as join4 } from "path";
535
- import { homedir as homedir3 } from "os";
536
-
537
- // src/services/logger.ts
538
- import { appendFileSync } from "fs";
539
- import { join as join3, dirname as dirname3 } from "path";
540
- import { homedir as homedir2 } from "os";
541
-
542
- // src/services/log-rotator.ts
543
- import { statSync, renameSync as renameSync2, readdirSync, unlinkSync as unlinkSync2 } from "fs";
544
- import { spawn } from "child_process";
545
- import { dirname as dirname2, join as join2 } from "path";
546
-
547
- // src/core/constants.ts
548
- var DEFAULT_FETCH_TIMEOUT_MS = 5000;
549
- var EXIT_BUFFER_MS = 50;
550
- var STALENESS_THRESHOLD_MINUTES = 5;
551
- var VERY_STALE_THRESHOLD_MINUTES = 30;
552
- var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
553
- var GC_MAX_CACHE_FILES = 20;
554
- var GC_ORPHAN_TMP_AGE_MS = 60 * 60 * 1000;
555
- var LOG_ROTATION_PROBABILITY = 0.05;
556
- var LOG_MAX_SIZE_BYTES = 512 * 1024;
557
- var LOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
558
- var LOG_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
559
-
560
- // src/services/log-rotator.ts
561
- var ARCHIVE_LOG_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log$/;
562
- var ARCHIVE_GZ_RE = /^debug\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}\.log\.gz$/;
563
- function archiveName(logPath, now = new Date) {
564
- const pad = (n) => n.toString().padStart(2, "0");
565
- const y = now.getFullYear();
566
- const mo = pad(now.getMonth() + 1);
567
- const d = pad(now.getDate());
568
- const h = pad(now.getHours());
569
- const min = pad(now.getMinutes());
570
- return join2(dirname2(logPath), `debug.${y}-${mo}-${d}T${h}-${min}.log`);
571
- }
572
- function spawnGzip(filePath) {
573
- try {
574
- const child = spawn("gzip", ["-f", filePath], {
575
- detached: true,
576
- stdio: "ignore"
577
- });
578
- child.unref();
579
- } catch {}
580
- }
581
- function runCleanup(logDir, excludePath) {
582
- try {
583
- const files = readdirSync(logDir);
584
- const now = Date.now();
585
- for (const name of files) {
586
- const filePath = join2(logDir, name);
587
- if (filePath === excludePath)
588
- continue;
589
- if (ARCHIVE_LOG_RE.test(name)) {
590
- const s = statSync(filePath, { throwIfNoEntry: false });
591
- if (s && now - s.mtimeMs >= LOG_MAX_AGE_MS) {
592
- spawnGzip(filePath);
593
- }
594
- continue;
595
- }
596
- if (ARCHIVE_GZ_RE.test(name)) {
597
- const s = statSync(filePath, { throwIfNoEntry: false });
598
- if (s && now - s.mtimeMs >= LOG_RETENTION_MS) {
599
- try {
600
- unlinkSync2(filePath);
601
- } catch {}
602
- }
603
- }
604
- }
605
- } catch {}
606
- }
607
- function maybeRotateLogs(logPath) {
608
- if (Math.random() > LOG_ROTATION_PROBABILITY)
609
- return;
610
- const logDir = dirname2(logPath);
611
- const stat = statSync(logPath, { throwIfNoEntry: false });
612
- let rotatedArchive = null;
613
- if (stat) {
614
- const age = Date.now() - stat.mtimeMs;
615
- const archive = archiveName(logPath);
616
- try {
617
- if (age >= LOG_MAX_AGE_MS) {
618
- renameSync2(logPath, archive);
619
- spawnGzip(archive);
620
- rotatedArchive = archive;
621
- } else if (stat.size >= LOG_MAX_SIZE_BYTES) {
622
- renameSync2(logPath, archive);
623
- rotatedArchive = archive;
624
- }
625
- } catch {}
626
- }
627
- runCleanup(logDir, rotatedArchive);
628
- }
629
-
630
- // src/services/logger.ts
631
- class Logger {
632
- enabled;
633
- logPath;
634
- constructor() {
635
- this.enabled = !!(process.env["DEBUG"] || process.env["CC_STATUSLINE_DEBUG"]);
636
- const logDir = process.env["CC_API_STATUSLINE_LOG_DIR"] || join3(homedir2(), ".claude", "cc-api-statusline");
637
- this.logPath = join3(logDir, "debug.log");
638
- if (this.enabled) {
639
- this.ensureLogDir();
640
- maybeRotateLogs(this.logPath);
641
- }
642
- }
643
- ensureLogDir() {
644
- try {
645
- const dir = dirname3(this.logPath);
646
- ensureDir(dir);
647
- } catch {
648
- this.enabled = false;
649
- }
650
- }
651
- formatLocalTimestamp() {
652
- const d = new Date;
653
- const pad = (n, len = 2) => n.toString().padStart(len, "0");
654
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
655
- }
656
- format(level, message, data) {
657
- const timestamp = this.formatLocalTimestamp();
658
- const dataStr = data ? ` ${JSON.stringify(data)}` : "";
659
- return `[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}
660
- `;
661
- }
662
- write(level, message, data) {
663
- if (!this.enabled) {
664
- return;
665
- }
666
- try {
667
- const entry = this.format(level, message, data);
668
- appendFileSync(this.logPath, entry, { encoding: "utf-8" });
669
- } catch {}
670
- }
671
- debug(message, data) {
672
- this.write("debug", message, data);
673
- }
674
- info(message, data) {
675
- this.write("info", message, data);
676
- }
677
- warn(message, data) {
678
- this.write("warn", message, data);
679
- }
680
- error(message, data) {
681
- this.write("error", message, data);
682
- }
683
- isEnabled() {
684
- return this.enabled;
685
- }
686
- getLogPath() {
687
- return this.logPath;
688
- }
593
+ var COMPONENT_FULL_LABELS = {
594
+ daily: "Daily",
595
+ weekly: "Weekly",
596
+ monthly: "Monthly",
597
+ balance: "Balance",
598
+ tokens: "Tokens",
599
+ rateLimit: "Rate",
600
+ plan: "Plan"
601
+ };
602
+ var COMPONENT_EMOJI_LABELS = {
603
+ daily: "\uD83D\uDCC5",
604
+ weekly: "\uD83D\uDCC6",
605
+ monthly: "\uD83D\uDDD3️",
606
+ balance: "\uD83D\uDCB0",
607
+ tokens: "\uD83D\uDD22",
608
+ rateLimit: "⚡",
609
+ plan: "\uD83D\uDCCB"
610
+ };
611
+ var COMPONENT_NERD_LABELS = {
612
+ daily: "",
613
+ weekly: "",
614
+ monthly: "",
615
+ balance: "",
616
+ tokens: "",
617
+ rateLimit: "",
618
+ plan: ""
619
+ };
620
+ var DEFAULT_COMPONENT_ORDER = [
621
+ "daily",
622
+ "weekly",
623
+ "monthly",
624
+ "balance",
625
+ "tokens",
626
+ "rateLimit",
627
+ "plan"
628
+ ];
629
+ // src/types/cache.ts
630
+ var CACHE_VERSION = 2;
631
+ function isCacheEntry(value) {
632
+ if (typeof value !== "object" || value === null)
633
+ return false;
634
+ const c = value;
635
+ return typeof c["version"] === "number" && typeof c["provider"] === "string" && typeof c["baseUrl"] === "string" && typeof c["tokenHash"] === "string" && typeof c["configHash"] === "string" && typeof c["endpointConfigHash"] === "string" && typeof c["data"] === "object" && c["data"] !== null && typeof c["renderedLine"] === "string" && typeof c["fetchedAt"] === "string" && typeof c["ttlSeconds"] === "number" && (c["errorState"] === null || typeof c["errorState"] === "object");
636
+ }
637
+ var PROVIDER_DETECTION_TTL_SECONDS = 86400;
638
+ function isProviderDetectionCacheEntry(value) {
639
+ if (typeof value !== "object" || value === null)
640
+ return false;
641
+ const c = value;
642
+ return typeof c["baseUrl"] === "string" && typeof c["provider"] === "string" && (c["detectedVia"] === "health-probe" || c["detectedVia"] === "url-pattern" || c["detectedVia"] === "override") && typeof c["detectedAt"] === "string" && typeof c["ttlSeconds"] === "number";
689
643
  }
690
- var logger = new Logger;
691
-
692
644
  // src/services/endpoint-config.ts
645
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
646
+ import { join as join5 } from "path";
693
647
  function getEndpointConfigDir(customRoot) {
694
648
  const envRoot = process.env["CC_API_STATUSLINE_CONFIG_DIR"];
695
- const root = customRoot || envRoot || join4(homedir3(), ".claude", "cc-api-statusline");
696
- return join4(root, "api-config");
649
+ const root = customRoot || envRoot || getConfigDir();
650
+ return join5(root, "api-config");
697
651
  }
698
652
  function loadEndpointConfigs(customDir) {
699
653
  const configDir = getEndpointConfigDir(customDir);
700
- if (!existsSync5(configDir)) {
654
+ if (!existsSync3(configDir)) {
701
655
  return getBuiltInEndpointConfigs();
702
656
  }
703
657
  const registry = {};
@@ -706,7 +660,7 @@ function loadEndpointConfigs(customDir) {
706
660
  return getBuiltInEndpointConfigs();
707
661
  }
708
662
  for (const file of files) {
709
- const filePath = join4(configDir, file);
663
+ const filePath = join5(configDir, file);
710
664
  try {
711
665
  const config = loadEndpointConfigFile(filePath);
712
666
  registry[config.provider] = config;
@@ -771,20 +725,20 @@ function validateEndpointConfig(data, filename) {
771
725
  }
772
726
  function computeEndpointConfigHash(customDir) {
773
727
  const configDir = getEndpointConfigDir(customDir);
774
- if (!existsSync5(configDir)) {
728
+ if (!existsSync3(configDir)) {
775
729
  const builtIn = getBuiltInEndpointConfigs();
776
730
  const serialized = JSON.stringify(builtIn, Object.keys(builtIn).sort());
777
- return sha256(serialized).slice(0, 12);
731
+ return shortHash(serialized, 12);
778
732
  }
779
733
  const files = readdirSync2(configDir).filter((f) => f.endsWith(".json")).sort();
780
734
  if (files.length === 0) {
781
735
  const builtIn = getBuiltInEndpointConfigs();
782
736
  const serialized = JSON.stringify(builtIn, Object.keys(builtIn).sort());
783
- return sha256(serialized).slice(0, 12);
737
+ return shortHash(serialized, 12);
784
738
  }
785
739
  let combined = "";
786
740
  for (const file of files) {
787
- const filePath = join4(configDir, file);
741
+ const filePath = join5(configDir, file);
788
742
  try {
789
743
  const content = readFileSync3(filePath, "utf-8");
790
744
  combined += `\x00${file}\x00${content}`;
@@ -792,7 +746,7 @@ function computeEndpointConfigHash(customDir) {
792
746
  continue;
793
747
  }
794
748
  }
795
- return sha256(combined).slice(0, 12);
749
+ return shortHash(combined, 12);
796
750
  }
797
751
  function getBuiltInEndpointConfigs() {
798
752
  return {
@@ -889,22 +843,26 @@ function getBuiltInEndpointConfigs() {
889
843
  }
890
844
 
891
845
  // src/services/endpoint-lock.ts
892
- import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
893
- import { join as join5 } from "path";
894
- import { homedir as homedir4 } from "os";
846
+ import { readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
847
+ import { join as join6 } from "path";
895
848
  function getLockFilePath(customDir) {
896
849
  if (customDir) {
897
- return join5(customDir, ".endpoint-config.lock");
850
+ return join6(customDir, ".endpoint-config.lock");
898
851
  }
899
- return join5(homedir4(), ".claude", "cc-api-statusline", ".endpoint-config.lock");
852
+ return join6(getConfigDir(), ".endpoint-config.lock");
900
853
  }
901
854
  function readEndpointLock(customDir) {
902
855
  const lockPath = getLockFilePath(customDir);
903
- if (!existsSync6(lockPath)) {
904
- return null;
856
+ let content;
857
+ try {
858
+ content = readFileSync4(lockPath, "utf-8");
859
+ } catch (err) {
860
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
861
+ return null;
862
+ }
863
+ throw err;
905
864
  }
906
865
  try {
907
- const content = readFileSync4(lockPath, "utf-8");
908
866
  const data = JSON.parse(content);
909
867
  if (typeof data === "object" && data !== null && "hash" in data && "lockedAt" in data && typeof data.hash === "string" && typeof data.lockedAt === "string") {
910
868
  return {
@@ -929,6 +887,90 @@ function writeEndpointLock(hash, customDir) {
929
887
  });
930
888
  }
931
889
 
890
+ // src/services/config.ts
891
+ import { readFileSync as readFileSync5 } from "fs";
892
+ import { join as join7 } from "path";
893
+ function getConfigPath(customPath) {
894
+ if (customPath) {
895
+ return customPath;
896
+ }
897
+ return join7(getConfigDir(), "config.json");
898
+ }
899
+ function deepMerge(target, source) {
900
+ const result = { ...target };
901
+ for (const key in source) {
902
+ const sourceValue = source[key];
903
+ const targetValue = result[key];
904
+ if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
905
+ result[key] = deepMerge(targetValue, sourceValue);
906
+ } else if (sourceValue !== undefined) {
907
+ result[key] = sourceValue;
908
+ }
909
+ }
910
+ return result;
911
+ }
912
+ function validateConfig(config) {
913
+ let maxWidth = config.display.maxWidth;
914
+ let pollIntervalSeconds = config.pollIntervalSeconds;
915
+ let pipedRequestTimeoutMs = config.pipedRequestTimeoutMs;
916
+ if (maxWidth < 20) {
917
+ logger.warn("display.maxWidth < 20, clamping to 20");
918
+ maxWidth = 20;
919
+ }
920
+ if (maxWidth > 100) {
921
+ logger.warn("display.maxWidth > 100, clamping to 100");
922
+ maxWidth = 100;
923
+ }
924
+ if (pollIntervalSeconds !== undefined && pollIntervalSeconds < 5) {
925
+ logger.warn("pollIntervalSeconds < 5, clamping to 5");
926
+ pollIntervalSeconds = 5;
927
+ }
928
+ if (pipedRequestTimeoutMs !== undefined && pipedRequestTimeoutMs < 100) {
929
+ logger.warn("pipedRequestTimeoutMs < 100, clamping to 100");
930
+ pipedRequestTimeoutMs = 100;
931
+ }
932
+ return {
933
+ ...config,
934
+ display: {
935
+ ...config.display,
936
+ maxWidth
937
+ },
938
+ pollIntervalSeconds,
939
+ pipedRequestTimeoutMs
940
+ };
941
+ }
942
+ function parseConfigContent(content, path) {
943
+ try {
944
+ const userConfig = JSON.parse(content);
945
+ const merged = deepMerge(DEFAULT_CONFIG, userConfig);
946
+ return validateConfig(merged);
947
+ } catch (err) {
948
+ logger.warn(`Could not load config from ${path}: ${err}`);
949
+ logger.warn("Using default configuration");
950
+ return DEFAULT_CONFIG;
951
+ }
952
+ }
953
+ function loadConfigWithHash(configPath) {
954
+ const path = getConfigPath(configPath);
955
+ let content;
956
+ try {
957
+ content = readFileSync5(path, "utf-8");
958
+ } catch (err) {
959
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
960
+ return { config: DEFAULT_CONFIG, configHash: shortHash("", 12) };
961
+ }
962
+ throw err;
963
+ }
964
+ return {
965
+ config: parseConfigContent(content, path),
966
+ configHash: shortHash(content, 12)
967
+ };
968
+ }
969
+ function serializableConfig(config) {
970
+ const { colors: _colors, ...rest } = config;
971
+ return rest;
972
+ }
973
+
932
974
  // src/services/config-defaults.ts
933
975
  function getDefaultStyleConfig() {
934
976
  return DEFAULT_CONFIG;
@@ -948,26 +990,26 @@ function getDefaultCrsConfig() {
948
990
  return config;
949
991
  }
950
992
  function writeDefaultConfigs(customDir) {
951
- const configDir = customDir || join6(homedir5(), ".claude", "cc-api-statusline");
952
- const configPath = join6(configDir, "config.json");
993
+ const configDir = customDir || getConfigDir();
994
+ const configPath = join8(configDir, "config.json");
953
995
  const apiConfigDir = getEndpointConfigDir(customDir);
954
996
  ensureDir(configDir);
955
997
  ensureDir(apiConfigDir);
956
- if (!existsSync7(configPath)) {
957
- const styleConfig = getDefaultStyleConfig();
958
- atomicWriteFile(configPath, JSON.stringify(styleConfig, null, 2), {
998
+ if (!existsSync4(configPath)) {
999
+ const styleConfigWithoutColors = serializableConfig(getDefaultStyleConfig());
1000
+ atomicWriteFile(configPath, JSON.stringify(styleConfigWithoutColors, null, 2), {
959
1001
  appendNewline: true
960
1002
  });
961
1003
  }
962
- const sub2apiPath = join6(apiConfigDir, "sub2api.json");
963
- if (!existsSync7(sub2apiPath)) {
1004
+ const sub2apiPath = join8(apiConfigDir, "sub2api.json");
1005
+ if (!existsSync4(sub2apiPath)) {
964
1006
  const sub2apiConfig = getDefaultSub2apiConfig();
965
1007
  atomicWriteFile(sub2apiPath, JSON.stringify(sub2apiConfig, null, 2), {
966
1008
  appendNewline: true
967
1009
  });
968
1010
  }
969
- const crsPath = join6(apiConfigDir, "crs.json");
970
- if (!existsSync7(crsPath)) {
1011
+ const crsPath = join8(apiConfigDir, "crs.json");
1012
+ if (!existsSync4(crsPath)) {
971
1013
  const crsConfig = getDefaultCrsConfig();
972
1014
  atomicWriteFile(crsPath, JSON.stringify(crsConfig, null, 2), {
973
1015
  appendNewline: true
@@ -977,10 +1019,10 @@ function writeDefaultConfigs(customDir) {
977
1019
  writeEndpointLock(currentHash, customDir);
978
1020
  }
979
1021
  function needsConfigInit(customDir) {
980
- const configDir = customDir || join6(homedir5(), ".claude", "cc-api-statusline");
981
- const configPath = join6(configDir, "config.json");
1022
+ const configDir = customDir || getConfigDir();
1023
+ const configPath = join8(configDir, "config.json");
982
1024
  const apiConfigDir = getEndpointConfigDir(customDir);
983
- return !existsSync7(configPath) || !existsSync7(apiConfigDir);
1025
+ return !existsSync4(configPath) || !existsSync4(apiConfigDir);
984
1026
  }
985
1027
 
986
1028
  // src/providers/http.ts
@@ -1126,15 +1168,14 @@ async function probeHealth(baseUrl, timeoutMs = 1500) {
1126
1168
  }
1127
1169
 
1128
1170
  // src/services/cache.ts
1129
- import { readFileSync as readFileSync5, existsSync as existsSync8, unlinkSync as unlinkSync4 } from "fs";
1130
- import { join as join7 } from "path";
1131
- import { homedir as homedir6 } from "os";
1171
+ import { readFileSync as readFileSync6, unlinkSync as unlinkSync4 } from "fs";
1172
+ import { join as join9 } from "path";
1132
1173
  function getCacheDir() {
1133
1174
  const override = process.env["CC_API_STATUSLINE_CACHE_DIR"];
1134
1175
  if (override) {
1135
1176
  return override;
1136
1177
  }
1137
- return join7(homedir6(), ".claude", "cc-api-statusline");
1178
+ return getConfigDir();
1138
1179
  }
1139
1180
  function ensureCacheDir() {
1140
1181
  const dir = getCacheDir();
@@ -1142,23 +1183,28 @@ function ensureCacheDir() {
1142
1183
  }
1143
1184
  function getCachePath(baseUrl) {
1144
1185
  const hash = shortHash(baseUrl, 12);
1145
- return join7(getCacheDir(), `cache-${hash}.json`);
1186
+ return join9(getCacheDir(), `cache-${hash}.json`);
1146
1187
  }
1147
1188
  function readCache(baseUrl) {
1148
1189
  const path = getCachePath(baseUrl);
1149
- if (!existsSync8(path)) {
1150
- return null;
1190
+ let content;
1191
+ try {
1192
+ content = readFileSync6(path, "utf-8");
1193
+ } catch (err) {
1194
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1195
+ return null;
1196
+ }
1197
+ throw err;
1151
1198
  }
1152
1199
  try {
1153
- const content = readFileSync5(path, "utf-8");
1154
1200
  const data = JSON.parse(content);
1155
1201
  if (!isCacheEntry(data)) {
1156
- console.warn(`Invalid cache structure at ${path}`);
1202
+ logger.warn(`Invalid cache structure at ${path}`);
1157
1203
  return null;
1158
1204
  }
1159
1205
  return data;
1160
- } catch (error) {
1161
- console.warn(`Failed to read cache from ${path}: ${error}`);
1206
+ } catch (err) {
1207
+ logger.warn(`Failed to parse cache from ${path}: ${err}`);
1162
1208
  return null;
1163
1209
  }
1164
1210
  }
@@ -1169,7 +1215,7 @@ function writeCache(baseUrl, entry) {
1169
1215
  const content = JSON.stringify(entry, null, 2);
1170
1216
  atomicWriteFile(path, content);
1171
1217
  } catch (error) {
1172
- console.warn(`Failed to write cache to ${path}: ${error}`);
1218
+ logger.warn(`Failed to write cache to ${path}: ${error}`);
1173
1219
  }
1174
1220
  }
1175
1221
  function isCacheValid(entry, currentEnv) {
@@ -1197,18 +1243,6 @@ function isCacheProviderValid(entry, currentProvider) {
1197
1243
  function isCacheRenderedLineUsable(entry, currentConfigHash) {
1198
1244
  return entry.configHash === currentConfigHash;
1199
1245
  }
1200
- function computeConfigHash(configPath) {
1201
- if (!existsSync8(configPath)) {
1202
- return sha256("").slice(0, 12);
1203
- }
1204
- try {
1205
- const bytes = readFileSync5(configPath);
1206
- return shortHash(bytes.toString("utf-8"), 12);
1207
- } catch (error) {
1208
- console.warn(`Failed to read config for hash: ${error}`);
1209
- return sha256("").slice(0, 12);
1210
- }
1211
- }
1212
1246
  var DEFAULT_POLL_INTERVAL_SECONDS = 30;
1213
1247
  function getEffectivePollInterval(config, envOverride) {
1214
1248
  if (envOverride !== null) {
@@ -1219,18 +1253,23 @@ function getEffectivePollInterval(config, envOverride) {
1219
1253
  }
1220
1254
  function getProviderDetectionCachePath(baseUrl) {
1221
1255
  const hash = shortHash(baseUrl, 12);
1222
- return join7(getCacheDir(), `provider-detect-${hash}.json`);
1256
+ return join9(getCacheDir(), `provider-detect-${hash}.json`);
1223
1257
  }
1224
1258
  function readProviderDetectionCache(baseUrl) {
1225
1259
  const path = getProviderDetectionCachePath(baseUrl);
1226
- if (!existsSync8(path)) {
1227
- return null;
1260
+ let content;
1261
+ try {
1262
+ content = readFileSync6(path, "utf-8");
1263
+ } catch (err) {
1264
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1265
+ return null;
1266
+ }
1267
+ throw err;
1228
1268
  }
1229
1269
  try {
1230
- const content = readFileSync5(path, "utf-8");
1231
1270
  const data = JSON.parse(content);
1232
1271
  if (!isProviderDetectionCacheEntry(data)) {
1233
- console.warn(`Invalid provider detection cache structure at ${path}`);
1272
+ logger.warn(`Invalid provider detection cache structure at ${path}`);
1234
1273
  return null;
1235
1274
  }
1236
1275
  const detectedAt = new Date(data.detectedAt).getTime();
@@ -1244,8 +1283,8 @@ function readProviderDetectionCache(baseUrl) {
1244
1283
  return null;
1245
1284
  }
1246
1285
  return data;
1247
- } catch (error) {
1248
- console.warn(`Failed to read provider detection cache from ${path}: ${error}`);
1286
+ } catch (err) {
1287
+ logger.warn(`Failed to parse provider detection cache from ${path}: ${err}`);
1249
1288
  return null;
1250
1289
  }
1251
1290
  }
@@ -1256,13 +1295,15 @@ function writeProviderDetectionCache(baseUrl, entry) {
1256
1295
  const content = JSON.stringify(entry, null, 2);
1257
1296
  atomicWriteFile(path, content);
1258
1297
  } catch (error) {
1259
- console.warn(`Failed to write provider detection cache to ${path}: ${error}`);
1298
+ logger.warn(`Failed to write provider detection cache to ${path}: ${error}`);
1260
1299
  }
1261
1300
  }
1262
1301
 
1263
1302
  // src/providers/autodetect.ts
1264
1303
  var detectionCache = new Map;
1265
- function detectProviderFromUrlPattern(baseUrl, endpointConfigs = {}) {
1304
+ function detectProviderFromUrlPattern(baseUrl, endpointConfigs = {}, options = {}) {
1305
+ const includeBuiltInPatterns = options.includeBuiltInPatterns ?? true;
1306
+ const fallbackProvider = Object.prototype.hasOwnProperty.call(options, "fallbackProvider") ? options.fallbackProvider ?? null : "sub2api";
1266
1307
  const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
1267
1308
  for (const [providerId, config] of Object.entries(endpointConfigs)) {
1268
1309
  const urlPatterns = config.detection?.urlPatterns;
@@ -1275,10 +1316,10 @@ function detectProviderFromUrlPattern(baseUrl, endpointConfigs = {}) {
1275
1316
  }
1276
1317
  }
1277
1318
  }
1278
- if (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats")) {
1319
+ if (includeBuiltInPatterns && (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats"))) {
1279
1320
  return "claude-relay-service";
1280
1321
  }
1281
- return "sub2api";
1322
+ return fallbackProvider;
1282
1323
  }
1283
1324
  async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {}, probeTimeoutMs = 1500) {
1284
1325
  if (providerOverride) {
@@ -1302,19 +1343,14 @@ async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {},
1302
1343
  });
1303
1344
  return diskCached.provider;
1304
1345
  }
1305
- for (const [providerId, config] of Object.entries(endpointConfigs)) {
1306
- const urlPatterns = config.detection?.urlPatterns;
1307
- if (urlPatterns && urlPatterns.length > 0) {
1308
- const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
1309
- for (const pattern of urlPatterns) {
1310
- const normalizedPattern = pattern.toLowerCase();
1311
- if (normalizedUrl.includes(normalizedPattern)) {
1312
- logger.debug("Provider detected via endpoint URL pattern", { provider: providerId, pattern });
1313
- cacheProviderDetection(baseUrl, providerId, "url-pattern");
1314
- return providerId;
1315
- }
1316
- }
1317
- }
1346
+ const endpointPatternProvider = detectProviderFromUrlPattern(baseUrl, endpointConfigs, {
1347
+ includeBuiltInPatterns: false,
1348
+ fallbackProvider: null
1349
+ });
1350
+ if (endpointPatternProvider) {
1351
+ logger.debug("Provider detected via endpoint URL pattern", { provider: endpointPatternProvider });
1352
+ cacheProviderDetection(baseUrl, endpointPatternProvider, "url-pattern");
1353
+ return endpointPatternProvider;
1318
1354
  }
1319
1355
  logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
1320
1356
  const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
@@ -1324,6 +1360,11 @@ async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {},
1324
1360
  return probedProvider;
1325
1361
  }
1326
1362
  const patternProvider = detectProviderFromUrlPattern(baseUrl, {});
1363
+ if (!patternProvider) {
1364
+ logger.debug("Provider URL pattern detection had no match, defaulting to sub2api");
1365
+ cacheProviderDetection(baseUrl, "sub2api", "url-pattern");
1366
+ return "sub2api";
1367
+ }
1327
1368
  logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
1328
1369
  cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
1329
1370
  return patternProvider;
@@ -1347,8 +1388,8 @@ function clearDetectionCache() {
1347
1388
  }
1348
1389
 
1349
1390
  // src/cli/commands.ts
1350
- import { existsSync as existsSync9, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
1351
- import { join as join8 } from "path";
1391
+ import { existsSync as existsSync5, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
1392
+ import { join as join10 } from "path";
1352
1393
  function handleInstall(args) {
1353
1394
  const existing = getExistingStatusLine();
1354
1395
  if (existing && !args.force) {
@@ -1391,11 +1432,11 @@ function handleApplyConfig() {
1391
1432
  console.log("✓ Provider detection cache cleared");
1392
1433
  const cacheDir = getCacheDir();
1393
1434
  let failCount = 0;
1394
- if (existsSync9(cacheDir)) {
1435
+ if (existsSync5(cacheDir)) {
1395
1436
  const files = readdirSync3(cacheDir).filter((f) => f.startsWith("cache-") && f.endsWith(".json"));
1396
1437
  for (const file of files) {
1397
1438
  try {
1398
- const filePath = join8(cacheDir, file);
1439
+ const filePath = join10(cacheDir, file);
1399
1440
  unlinkSync5(filePath);
1400
1441
  } catch {
1401
1442
  failCount++;
@@ -1415,7 +1456,7 @@ function handleApplyConfig() {
1415
1456
  console.log("");
1416
1457
  console.log("Config files:");
1417
1458
  const apiConfigDir = getEndpointConfigDir();
1418
- if (existsSync9(apiConfigDir)) {
1459
+ if (existsSync5(apiConfigDir)) {
1419
1460
  const configFiles = readdirSync3(apiConfigDir).filter((f) => f.endsWith(".json"));
1420
1461
  for (const file of configFiles) {
1421
1462
  console.log(` - ${apiConfigDir}/${file}`);
@@ -1423,83 +1464,10 @@ function handleApplyConfig() {
1423
1464
  }
1424
1465
  process.exit(0);
1425
1466
  }
1426
- // src/services/config.ts
1427
- import { readFileSync as readFileSync6, existsSync as existsSync10 } from "fs";
1428
- import { join as join9 } from "path";
1429
- import { homedir as homedir7 } from "os";
1430
- function getConfigDir() {
1431
- return join9(homedir7(), ".claude", "cc-api-statusline");
1432
- }
1433
- function getConfigPath(customPath) {
1434
- if (customPath) {
1435
- return customPath;
1436
- }
1437
- return join9(getConfigDir(), "config.json");
1438
- }
1439
- function deepMerge(target, source) {
1440
- const result = { ...target };
1441
- for (const key in source) {
1442
- const sourceValue = source[key];
1443
- const targetValue = result[key];
1444
- if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
1445
- result[key] = deepMerge(targetValue, sourceValue);
1446
- } else if (sourceValue !== undefined) {
1447
- result[key] = sourceValue;
1448
- }
1449
- }
1450
- return result;
1451
- }
1452
- function validateConfig(config) {
1453
- let maxWidth = config.display.maxWidth;
1454
- let pollIntervalSeconds = config.pollIntervalSeconds;
1455
- let pipedRequestTimeoutMs = config.pipedRequestTimeoutMs;
1456
- if (maxWidth < 20) {
1457
- console.warn("Warning: display.maxWidth < 20, clamping to 20");
1458
- maxWidth = 20;
1459
- }
1460
- if (maxWidth > 100) {
1461
- console.warn("Warning: display.maxWidth > 100, clamping to 100");
1462
- maxWidth = 100;
1463
- }
1464
- if (pollIntervalSeconds !== undefined && pollIntervalSeconds < 5) {
1465
- console.warn("Warning: pollIntervalSeconds < 5, clamping to 5");
1466
- pollIntervalSeconds = 5;
1467
- }
1468
- if (pipedRequestTimeoutMs !== undefined && pipedRequestTimeoutMs < 100) {
1469
- console.warn("Warning: pipedRequestTimeoutMs < 100, clamping to 100");
1470
- pipedRequestTimeoutMs = 100;
1471
- }
1472
- return {
1473
- ...config,
1474
- display: {
1475
- ...config.display,
1476
- maxWidth
1477
- },
1478
- pollIntervalSeconds,
1479
- pipedRequestTimeoutMs
1480
- };
1481
- }
1482
- function loadConfig(configPath) {
1483
- const path = getConfigPath(configPath);
1484
- if (!existsSync10(path)) {
1485
- return DEFAULT_CONFIG;
1486
- }
1487
- try {
1488
- const content = readFileSync6(path, "utf-8");
1489
- const userConfig = JSON.parse(content);
1490
- const merged = deepMerge(DEFAULT_CONFIG, userConfig);
1491
- return validateConfig(merged);
1492
- } catch (error) {
1493
- console.warn(`Warning: Could not load config from ${path}: ${error}`);
1494
- console.warn("Using default configuration");
1495
- return DEFAULT_CONFIG;
1496
- }
1497
- }
1498
-
1499
1467
  // src/services/user-agent.ts
1500
1468
  import { execSync as execSync2 } from "child_process";
1501
- import { join as join10 } from "path";
1502
- import { homedir as homedir8 } from "os";
1469
+ import { join as join11 } from "path";
1470
+ import { homedir as homedir3 } from "os";
1503
1471
  var FALLBACK_UA = "claude-cli/2.1.56 (external, cli)";
1504
1472
  function resolveUserAgent(config) {
1505
1473
  if (!config) {
@@ -1526,10 +1494,10 @@ function detectClaudeVersion() {
1526
1494
  if (!process.env["CLAUDECODE"]) {
1527
1495
  return null;
1528
1496
  }
1529
- const claudePath = join10(homedir8(), ".claude", "bin", "claude");
1497
+ const claudePath = join11(homedir3(), ".claude", "bin", "claude");
1530
1498
  const result = execSync2(`"${claudePath}" --version`, {
1531
1499
  encoding: "utf-8",
1532
- timeout: 1000,
1500
+ timeout: 100,
1533
1501
  stdio: ["ignore", "pipe", "ignore"]
1534
1502
  });
1535
1503
  const match = result.match(/(\d+\.\d+\.\d+)/);
@@ -1844,19 +1812,17 @@ function mapResponseToUsage(responseData, mapping, endpointConfig) {
1844
1812
  remainingSeconds: extractNumber(responseData, mapping["rateLimit.remainingSeconds"]) ?? 0
1845
1813
  };
1846
1814
  })();
1847
- const resetsAt = (() => {
1848
- const times = [];
1849
- if (daily?.resetsAt)
1850
- times.push(daily.resetsAt);
1851
- if (weekly?.resetsAt)
1852
- times.push(weekly.resetsAt);
1853
- if (monthly?.resetsAt)
1854
- times.push(monthly.resetsAt);
1855
- if (times.length === 0)
1856
- return null;
1857
- const sorted = [...times].sort();
1858
- return sorted[0] ?? null;
1859
- })();
1815
+ const resetsAt = computeSoonestReset({
1816
+ ...base,
1817
+ resetSemantics: billingMode === "balance" ? "expiry" : "end-of-day",
1818
+ balance,
1819
+ daily,
1820
+ weekly,
1821
+ monthly,
1822
+ tokenStats,
1823
+ rateLimit,
1824
+ resetsAt: null
1825
+ });
1860
1826
  return {
1861
1827
  ...base,
1862
1828
  resetSemantics: billingMode === "balance" ? "expiry" : "end-of-day",
@@ -1871,7 +1837,7 @@ function mapResponseToUsage(responseData, mapping, endpointConfig) {
1871
1837
  }
1872
1838
 
1873
1839
  // src/providers/endpoint-fetch.ts
1874
- function validateEndpointConfig2(config) {
1840
+ function validateEndpointConfigSemantics(config) {
1875
1841
  if (!config.provider)
1876
1842
  return "Endpoint config missing required field: provider";
1877
1843
  if (!config.endpoint?.path)
@@ -1894,7 +1860,7 @@ function validateEndpointConfig2(config) {
1894
1860
  return null;
1895
1861
  }
1896
1862
  async function fetchEndpoint(baseUrl, token, appConfig, endpointConfig, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
1897
- const validationError = validateEndpointConfig2(endpointConfig);
1863
+ const validationError = validateEndpointConfigSemantics(endpointConfig);
1898
1864
  if (validationError) {
1899
1865
  throw new Error(`Invalid endpoint config: ${validationError}`);
1900
1866
  }
@@ -2099,6 +2065,9 @@ function hexToRgb(hex) {
2099
2065
  function dimText(text) {
2100
2066
  return `${ANSI_DIM}${text}${ANSI_RESET}`;
2101
2067
  }
2068
+ function stripAnsi(text) {
2069
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
2070
+ }
2102
2071
  function resolveColor(colorName, usagePercent, config) {
2103
2072
  const effectiveColor = colorName ?? "auto";
2104
2073
  if (effectiveColor.startsWith("#") || ANSI_COLORS[effectiveColor.toLowerCase()]) {
@@ -2493,46 +2462,49 @@ function formatCompactNumber(n) {
2493
2462
 
2494
2463
  // src/renderer/component.ts
2495
2464
  function renderComponent(componentId, data, componentConfig, globalConfig, renderContext) {
2496
- const effectiveLayout = componentConfig.layout ?? globalConfig.display.layout;
2497
- const effectiveDisplayMode = resolveEffectiveDisplayMode(componentConfig.displayMode ?? globalConfig.display.displayMode, renderContext);
2498
- const effectiveProgressStyle = resolveEffectiveProgressStyle(componentConfig.progressStyle ?? globalConfig.display.progressStyle, renderContext);
2499
- const effectiveBarSize = componentConfig.barSize ?? globalConfig.display.barSize;
2500
- const effectiveBarStyle = componentConfig.barStyle ?? globalConfig.display.barStyle;
2501
- const clockFormat = globalConfig.display.clockFormat;
2465
+ const options = {
2466
+ layout: componentConfig.layout ?? globalConfig.display.layout,
2467
+ displayMode: resolveEffectiveDisplayMode(componentConfig.displayMode ?? globalConfig.display.displayMode, renderContext),
2468
+ progressStyle: resolveEffectiveProgressStyle(componentConfig.progressStyle ?? globalConfig.display.progressStyle, renderContext),
2469
+ barSize: componentConfig.barSize ?? globalConfig.display.barSize,
2470
+ barStyle: componentConfig.barStyle ?? globalConfig.display.barStyle,
2471
+ clockFormat: globalConfig.display.clockFormat
2472
+ };
2502
2473
  switch (componentId) {
2503
2474
  case "daily":
2504
- return renderQuotaComponent("daily", data.daily, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
2505
2475
  case "weekly":
2506
- return renderQuotaComponent("weekly", data.weekly, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
2507
2476
  case "monthly":
2508
- return renderQuotaComponent("monthly", data.monthly, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
2477
+ return renderQuotaComponent(componentId, data[componentId], options, componentConfig, globalConfig, renderContext);
2509
2478
  case "balance":
2510
- return renderBalanceComponent(data.balance, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, renderContext);
2479
+ return renderBalanceComponent(data.balance, options, componentConfig, globalConfig, renderContext);
2511
2480
  case "tokens":
2512
- return renderTokensComponent(data.tokenStats, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig, renderContext);
2481
+ return renderTokensComponent(data.tokenStats, options, componentConfig, globalConfig, renderContext);
2513
2482
  case "rateLimit":
2514
- return renderRateLimitComponent(data.rateLimit, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, renderContext);
2483
+ return renderRateLimitComponent(data.rateLimit, options, componentConfig, globalConfig, renderContext);
2515
2484
  case "plan":
2516
- return renderPlanComponent(data.planName, effectiveLayout, effectiveDisplayMode, componentConfig, globalConfig, renderContext);
2485
+ return renderPlanComponent(data.planName, options, componentConfig, globalConfig, renderContext);
2517
2486
  default:
2518
2487
  return null;
2519
2488
  }
2520
2489
  }
2521
- function renderQuotaComponent(componentId, quota, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, clockFormat, renderContext) {
2490
+ function renderQuotaComponent(componentId, quota, options, componentConfig, globalConfig, renderContext) {
2491
+ const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
2522
2492
  if (!quota)
2523
2493
  return null;
2524
2494
  const usagePercent = calculateUsagePercent(quota.used, quota.limit);
2525
2495
  const label = renderLabel(componentId, displayMode, componentConfig, quota.qualifier);
2496
+ const showPercentage = componentConfig.percentage !== false;
2526
2497
  const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
2527
- const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
2498
+ const valueColor = showPercentage ? resolvePartColor("value", usagePercent, componentConfig, globalConfig) : null;
2528
2499
  const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
2529
2500
  const countdownColor = resolvePartColor("countdown", usagePercent, componentConfig, globalConfig);
2530
2501
  const progress = renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, null, renderContext);
2531
- const value = ansiColor(`${Math.round(usagePercent)}%`, valueColor, renderContext);
2502
+ const value = showPercentage ? ansiColor(`${Math.round(usagePercent)}%`, valueColor, renderContext) : "";
2532
2503
  const countdown = renderSecondaryDisplay(quota.resetsAt, quota, componentConfig.countdown, countdownColor, clockFormat, renderContext);
2533
2504
  return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
2534
2505
  }
2535
- function renderBalanceComponent(balance, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, renderContext) {
2506
+ function renderBalanceComponent(balance, options, componentConfig, globalConfig, renderContext) {
2507
+ const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
2536
2508
  if (!balance)
2537
2509
  return null;
2538
2510
  const isUnlimited = balance.remaining === -1;
@@ -2551,7 +2523,8 @@ function renderBalanceComponent(balance, layout, displayMode, progressStyle, bar
2551
2523
  const countdown = "";
2552
2524
  return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
2553
2525
  }
2554
- function renderTokensComponent(tokenStats, layout, displayMode, componentConfig, globalConfig, renderContext) {
2526
+ function renderTokensComponent(tokenStats, options, componentConfig, globalConfig, renderContext) {
2527
+ const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
2555
2528
  if (!tokenStats)
2556
2529
  return null;
2557
2530
  const stats = tokenStats.total ?? tokenStats.today;
@@ -2567,7 +2540,8 @@ function renderTokensComponent(tokenStats, layout, displayMode, componentConfig,
2567
2540
  const countdown = "";
2568
2541
  return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
2569
2542
  }
2570
- function renderRateLimitComponent(rateLimit, layout, displayMode, progressStyle, barSize, barStyle, componentConfig, globalConfig, renderContext) {
2543
+ function renderRateLimitComponent(rateLimit, options, componentConfig, globalConfig, renderContext) {
2544
+ const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
2571
2545
  if (!rateLimit)
2572
2546
  return null;
2573
2547
  let usagePercent = null;
@@ -2584,7 +2558,8 @@ function renderRateLimitComponent(rateLimit, layout, displayMode, progressStyle,
2584
2558
  const countdown = "";
2585
2559
  return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
2586
2560
  }
2587
- function renderPlanComponent(planName, layout, displayMode, componentConfig, globalConfig, renderContext) {
2561
+ function renderPlanComponent(planName, options, componentConfig, globalConfig, renderContext) {
2562
+ const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
2588
2563
  if (displayMode === "hidden")
2589
2564
  return null;
2590
2565
  const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
@@ -2698,21 +2673,25 @@ function assembleComponent(layout, label, labelColor, progress, value, countdown
2698
2673
  if (layout === "percent-first") {
2699
2674
  if (coloredLabel)
2700
2675
  parts.push(coloredLabel);
2701
- parts.push(value);
2676
+ if (value)
2677
+ parts.push(value);
2702
2678
  if (progress)
2703
2679
  parts.push(progress);
2704
- if (countdown)
2705
- parts.push(countdown);
2706
2680
  } else {
2707
2681
  if (coloredLabel)
2708
2682
  parts.push(coloredLabel);
2709
2683
  if (progress)
2710
2684
  parts.push(progress);
2711
- parts.push(value);
2712
- if (countdown)
2713
- parts.push(countdown);
2685
+ if (value)
2686
+ parts.push(value);
2714
2687
  }
2715
- return parts.filter((p) => p).join(" ").replace(/ (\x1b\[[0-9;]*m)? ([·•])/g, "$1 $2");
2688
+ if (countdown && parts.length > 0) {
2689
+ const idx = parts.length - 1;
2690
+ parts[idx] = (parts[idx] ?? "") + countdown;
2691
+ } else if (countdown) {
2692
+ parts.push(countdown);
2693
+ }
2694
+ return parts.join(" ");
2716
2695
  }
2717
2696
  function calculateUsagePercent(used, limit) {
2718
2697
  if (limit === null)
@@ -2749,8 +2728,7 @@ function computeMaxWidth(termWidth, maxWidthPct) {
2749
2728
  return Math.floor(termWidth * pct / 100);
2750
2729
  }
2751
2730
  function visibleLength(text) {
2752
- const stripped = text.replace(/\x1b\[[0-9;]*m/g, "");
2753
- return stripped.length;
2731
+ return stripAnsi(text).length;
2754
2732
  }
2755
2733
  function ansiAwareTruncate(text, maxWidth) {
2756
2734
  const visible = visibleLength(text);
@@ -2797,8 +2775,8 @@ var COMPONENT_DROP_PRIORITY = [
2797
2775
  // src/renderer/divider.ts
2798
2776
  function renderDivider(divider) {
2799
2777
  const text = divider.text ?? "|";
2800
- const padding = divider.padding ?? 1;
2801
- const pad = " ".repeat(padding);
2778
+ const margin = divider.margin ?? 1;
2779
+ const pad = " ".repeat(margin);
2802
2780
  const padded = `${pad}${text}${pad}`;
2803
2781
  return ansiColor(padded, divider.color ?? "#555753");
2804
2782
  }
@@ -2890,63 +2868,46 @@ function renderStatusline(data, config, errorState, cacheAge, isPiped = false) {
2890
2868
  currentWidth = calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge);
2891
2869
  }
2892
2870
  }
2893
- const renderedComponents = [];
2894
- for (const componentId of componentOrder) {
2895
- if (activeComponents.has(componentId)) {
2896
- const rendered = componentMap.get(componentId);
2897
- if (rendered)
2898
- renderedComponents.push(rendered);
2899
- }
2900
- }
2901
- let statusline = renderedComponents.join(separator);
2902
- if (errorState) {
2903
- if (isTransitionState(errorState)) {
2904
- statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
2905
- } else {
2906
- const hasCache = renderedComponents.length > 0;
2907
- const errorMode = hasCache ? "with-cache" : "without-cache";
2908
- const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
2909
- statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
2910
- }
2911
- }
2871
+ let statusline = assembleStatuslineString(componentOrder, componentMap, activeComponents, separator, errorState, data, cacheAge);
2912
2872
  const termWidth = getTerminalWidth();
2913
2873
  const maxW = computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
2914
2874
  statusline = ansiAwareTruncate(statusline, maxW);
2915
2875
  return statusline;
2916
2876
  }
2917
2877
  function computeSeparator(config) {
2918
- const dividerConfig = config.components.divider;
2878
+ const dividerConfig = config.display.divider;
2919
2879
  if (dividerConfig === false)
2920
2880
  return "";
2921
- if (typeof dividerConfig === "object")
2922
- return renderDivider(dividerConfig);
2923
- return config.display.separator ?? " | ";
2881
+ return renderDivider(dividerConfig ?? DEFAULT_DIVIDER_CONFIG);
2924
2882
  }
2925
2883
  function maxWidth(config) {
2926
2884
  const termWidth = getTerminalWidth();
2927
2885
  return computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
2928
2886
  }
2929
- function calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge) {
2930
- const components = [];
2887
+ function assembleStatuslineString(componentOrder, componentMap, activeComponents, separator, errorState, data, cacheAge) {
2888
+ const rendered = [];
2931
2889
  for (const id of componentOrder) {
2932
2890
  if (activeComponents.has(id)) {
2933
- const rendered = componentMap.get(id);
2934
- if (rendered)
2935
- components.push(rendered);
2891
+ const r = componentMap.get(id);
2892
+ if (r)
2893
+ rendered.push(r);
2936
2894
  }
2937
2895
  }
2938
- let statusline = components.join(separator);
2896
+ let statusline = rendered.join(separator);
2939
2897
  if (errorState) {
2940
2898
  if (isTransitionState(errorState)) {
2941
2899
  statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
2942
2900
  } else {
2943
- const hasCache = components.length > 0;
2901
+ const hasCache = rendered.length > 0;
2944
2902
  const errorMode = hasCache ? "with-cache" : "without-cache";
2945
2903
  const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
2946
2904
  statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
2947
2905
  }
2948
2906
  }
2949
- return visibleLength(statusline);
2907
+ return statusline;
2908
+ }
2909
+ function calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge) {
2910
+ return visibleLength(assembleStatuslineString(componentOrder, componentMap, activeComponents, separator, errorState, data, cacheAge));
2950
2911
  }
2951
2912
  function getComponentOrder(config) {
2952
2913
  const explicitOrder = [];
@@ -3094,11 +3055,11 @@ async function executeCycle(ctx) {
3094
3055
  }
3095
3056
  }
3096
3057
  // src/services/cache-gc.ts
3097
- import { readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync6, existsSync as existsSync11 } from "fs";
3098
- import { join as join11 } from "path";
3058
+ import { readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync6, existsSync as existsSync6 } from "fs";
3059
+ import { join as join12 } from "path";
3099
3060
  function runCacheGC(cacheDir) {
3100
3061
  try {
3101
- if (!existsSync11(cacheDir)) {
3062
+ if (!existsSync6(cacheDir)) {
3102
3063
  logger.debug("GC: Cache directory does not exist, skipping", { cacheDir });
3103
3064
  return;
3104
3065
  }
@@ -3109,7 +3070,7 @@ function runCacheGC(cacheDir) {
3109
3070
  const tmpFiles = [];
3110
3071
  for (const file of files) {
3111
3072
  try {
3112
- const filePath = join11(cacheDir, file);
3073
+ const filePath = join12(cacheDir, file);
3113
3074
  const stats = statSync2(filePath);
3114
3075
  const mtime = stats.mtimeMs;
3115
3076
  if (file.startsWith("cache-") && file.endsWith(".json")) {
@@ -3129,7 +3090,7 @@ function runCacheGC(cacheDir) {
3129
3090
  const age = now - file.mtime;
3130
3091
  if (age > GC_MAX_AGE_MS) {
3131
3092
  try {
3132
- unlinkSync6(join11(cacheDir, file.name));
3093
+ unlinkSync6(join12(cacheDir, file.name));
3133
3094
  deletedCount++;
3134
3095
  logger.debug("GC: Deleted old cache file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
3135
3096
  } catch (error) {
@@ -3141,7 +3102,7 @@ function runCacheGC(cacheDir) {
3141
3102
  const age = now - file.mtime;
3142
3103
  if (age > GC_MAX_AGE_MS) {
3143
3104
  try {
3144
- unlinkSync6(join11(cacheDir, file.name));
3105
+ unlinkSync6(join12(cacheDir, file.name));
3145
3106
  deletedCount++;
3146
3107
  logger.debug("GC: Deleted old provider-detect file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
3147
3108
  } catch (error) {
@@ -3153,7 +3114,7 @@ function runCacheGC(cacheDir) {
3153
3114
  const age = now - file.mtime;
3154
3115
  if (age > GC_ORPHAN_TMP_AGE_MS) {
3155
3116
  try {
3156
- unlinkSync6(join11(cacheDir, file.name));
3117
+ unlinkSync6(join12(cacheDir, file.name));
3157
3118
  deletedCount++;
3158
3119
  logger.debug("GC: Deleted orphaned tmp file", { file: file.name, ageMinutes: Math.floor(age / (60 * 1000)) });
3159
3120
  } catch (error) {
@@ -3170,7 +3131,7 @@ function runCacheGC(cacheDir) {
3170
3131
  const toDelete = remainingCacheFiles.slice(0, remainingCacheFiles.length - GC_MAX_CACHE_FILES);
3171
3132
  for (const file of toDelete) {
3172
3133
  try {
3173
- unlinkSync6(join11(cacheDir, file.name));
3134
+ unlinkSync6(join12(cacheDir, file.name));
3174
3135
  deletedCount++;
3175
3136
  logger.debug("GC: Deleted cache file (count limit)", { file: file.name });
3176
3137
  } catch (error) {
@@ -3192,6 +3153,11 @@ class StatuslineError extends Error {
3192
3153
  this.errorType = errorType;
3193
3154
  }
3194
3155
  }
3156
+ function safeStdoutWrite(data) {
3157
+ try {
3158
+ process.stdout["write"](data);
3159
+ } catch {}
3160
+ }
3195
3161
  function readAndValidateEnv() {
3196
3162
  const env = readCurrentEnv();
3197
3163
  logger.debug("Environment loaded", {
@@ -3206,7 +3172,7 @@ function readAndValidateEnv() {
3206
3172
  }
3207
3173
  const { baseUrl } = env;
3208
3174
  if (!baseUrl) {
3209
- process.exit(1);
3175
+ process.exit(0);
3210
3176
  }
3211
3177
  return { env, baseUrl };
3212
3178
  }
@@ -3216,13 +3182,6 @@ function ensureDefaultConfigs() {
3216
3182
  writeDefaultConfigs();
3217
3183
  }
3218
3184
  }
3219
- function loadConfigWithHash(configPath) {
3220
- const config = loadConfig(configPath);
3221
- const resolvedPath = getConfigPath(configPath);
3222
- const configHash = computeConfigHash(resolvedPath);
3223
- logger.debug("Config loaded", { configPath: resolvedPath, configHash });
3224
- return { config, configHash };
3225
- }
3226
3185
  function loadEndpointConfigsWithHash() {
3227
3186
  const endpointConfigs = loadEndpointConfigs();
3228
3187
  const endpointConfigHash = computeEndpointConfigHash();
@@ -3330,7 +3289,7 @@ async function executePipedMode(args) {
3330
3289
  logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
3331
3290
  const fallback = dimText("⟳ Refreshing...");
3332
3291
  const formatted = formatOutput(fallback, isPiped);
3333
- process.stdout.write(formatted);
3292
+ safeStdoutWrite(formatted);
3334
3293
  process.exit(0);
3335
3294
  }, watchdogMs).unref();
3336
3295
  }
@@ -3343,7 +3302,7 @@ async function executePipedMode(args) {
3343
3302
  const errorType = error instanceof StatuslineError ? error.errorType : "network-error";
3344
3303
  const errorOutput = renderError(errorType, "without-cache");
3345
3304
  const formattedOutput2 = formatOutput(errorOutput, isPiped);
3346
- process.stdout.write(formattedOutput2);
3305
+ safeStdoutWrite(formattedOutput2);
3347
3306
  logger.debug("=== cc-api-statusline execution completed ===");
3348
3307
  process.exit(0);
3349
3308
  }
@@ -3358,7 +3317,7 @@ async function executePipedMode(args) {
3358
3317
  logger.error("Execution cycle failed", { error: String(error) });
3359
3318
  const errorOutput = renderError("network-error", "without-cache");
3360
3319
  const formattedOutput2 = formatOutput(errorOutput, isPiped);
3361
- process.stdout.write(formattedOutput2);
3320
+ safeStdoutWrite(formattedOutput2);
3362
3321
  logger.debug("=== cc-api-statusline execution completed ===");
3363
3322
  process.exit(0);
3364
3323
  }
@@ -3370,7 +3329,7 @@ async function executePipedMode(args) {
3370
3329
  cacheUpdate: !!result.cacheUpdate
3371
3330
  });
3372
3331
  const formattedOutput = formatOutput(result.output, isPiped);
3373
- process.stdout.write(formattedOutput);
3332
+ safeStdoutWrite(formattedOutput);
3374
3333
  if (result.cacheUpdate) {
3375
3334
  writeCache(baseUrl, result.cacheUpdate);
3376
3335
  logger.debug("Cache written", { baseUrl });
@@ -3428,6 +3387,5 @@ process.on("uncaughtException", (error) => {
3428
3387
  });
3429
3388
  main().catch((error) => {
3430
3389
  logger.error("Unhandled error in main", { error: String(error) });
3431
- console.error(`Fatal error: ${error}`);
3432
- process.exit(1);
3390
+ process.exit(0);
3433
3391
  });