cc-api-statusline 0.1.2 → 0.1.4

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.
@@ -209,6 +209,13 @@ function isCacheEntry(value) {
209
209
  const c = value;
210
210
  return typeof c["version"] === "number" && typeof c["provider"] === "string" && typeof c["baseUrl"] === "string" && typeof c["tokenHash"] === "string" && typeof c["configHash"] === "string" && typeof c["data"] === "object" && c["data"] !== null && typeof c["renderedLine"] === "string" && typeof c["fetchedAt"] === "string" && typeof c["ttlSeconds"] === "number" && (c["errorState"] === null || typeof c["errorState"] === "object");
211
211
  }
212
+ var PROVIDER_DETECTION_TTL_SECONDS = 86400;
213
+ function isProviderDetectionCacheEntry(value) {
214
+ if (typeof value !== "object" || value === null)
215
+ return false;
216
+ const c = value;
217
+ 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";
218
+ }
212
219
  // src/services/cache.ts
213
220
  function getCacheDir() {
214
221
  const override = process.env["CC_API_STATUSLINE_CACHE_DIR"];
@@ -310,6 +317,58 @@ function getEffectivePollInterval(config, envOverride) {
310
317
  const fromConfig = config.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS;
311
318
  return Math.max(5, fromConfig);
312
319
  }
320
+ function getProviderDetectionCachePath(baseUrl) {
321
+ const hash = shortHash(baseUrl, 12);
322
+ return join2(getCacheDir(), `provider-detect-${hash}.json`);
323
+ }
324
+ function readProviderDetectionCache(baseUrl) {
325
+ const path = getProviderDetectionCachePath(baseUrl);
326
+ if (!existsSync2(path)) {
327
+ return null;
328
+ }
329
+ try {
330
+ const content = readFileSync2(path, "utf-8");
331
+ const data = JSON.parse(content);
332
+ if (!isProviderDetectionCacheEntry(data)) {
333
+ console.warn(`Invalid provider detection cache structure at ${path}`);
334
+ return null;
335
+ }
336
+ const detectedAt = new Date(data.detectedAt).getTime();
337
+ const now = Date.now();
338
+ const age = now - detectedAt;
339
+ const ttlMs = data.ttlSeconds * 1000;
340
+ if (age >= ttlMs) {
341
+ try {
342
+ unlinkSync(path);
343
+ } catch {}
344
+ return null;
345
+ }
346
+ return data;
347
+ } catch (error) {
348
+ console.warn(`Failed to read provider detection cache from ${path}: ${error}`);
349
+ return null;
350
+ }
351
+ }
352
+ function writeProviderDetectionCache(baseUrl, entry) {
353
+ const path = getProviderDetectionCachePath(baseUrl);
354
+ const tmpPath = `${path}.tmp`;
355
+ try {
356
+ ensureCacheDir();
357
+ 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);
363
+ } catch (error) {
364
+ console.warn(`Failed to write provider detection cache to ${path}: ${error}`);
365
+ try {
366
+ if (existsSync2(tmpPath)) {
367
+ unlinkSync(tmpPath);
368
+ }
369
+ } catch {}
370
+ }
371
+ }
313
372
 
314
373
  // src/services/config.ts
315
374
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
@@ -638,7 +697,7 @@ function detectClaudeVersion() {
638
697
  }
639
698
  }
640
699
 
641
- // src/providers/sub2api.ts
700
+ // src/services/time.ts
642
701
  function computeNextMidnightLocal() {
643
702
  const now = new Date;
644
703
  const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0);
@@ -656,6 +715,8 @@ function computeFirstOfNextMonthLocal() {
656
715
  const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0);
657
716
  return nextMonth.toISOString();
658
717
  }
718
+
719
+ // src/providers/sub2api.ts
659
720
  function mapPeriodTokens(data) {
660
721
  if (!data)
661
722
  return null;
@@ -672,14 +733,12 @@ function mapPeriodTokens(data) {
672
733
  function createQuotaWindow(used, limit, resetsAt) {
673
734
  if (used === undefined)
674
735
  return null;
675
- const actualLimit = limit === null || limit === undefined ? null : limit;
676
- let remaining = null;
677
- if (actualLimit !== null) {
678
- remaining = Math.max(0, actualLimit - used);
679
- }
736
+ if (limit === null || limit === undefined)
737
+ return null;
738
+ const remaining = Math.max(0, limit - used);
680
739
  return {
681
740
  used,
682
- limit: actualLimit,
741
+ limit,
683
742
  remaining,
684
743
  resetsAt
685
744
  };
@@ -775,6 +834,44 @@ async function fetchSub2api(baseUrl, token, config, timeoutMs = 5000) {
775
834
  }
776
835
  }
777
836
 
837
+ // src/providers/health-probe.ts
838
+ function extractOrigin(baseUrl) {
839
+ try {
840
+ const url = new URL(baseUrl);
841
+ return url.origin;
842
+ } catch {
843
+ return baseUrl;
844
+ }
845
+ }
846
+ async function probeHealth(baseUrl, timeoutMs = 1500) {
847
+ const origin = extractOrigin(baseUrl);
848
+ const healthUrl = `${origin}/health`;
849
+ logger.debug("Probing health endpoint", { healthUrl, timeoutMs });
850
+ try {
851
+ const responseText = await secureFetch(healthUrl, {
852
+ method: "GET",
853
+ headers: {
854
+ Accept: "application/json"
855
+ }
856
+ }, timeoutMs);
857
+ const data = JSON.parse(responseText);
858
+ logger.debug("Health probe response", { data });
859
+ if (typeof data["service"] === "string") {
860
+ logger.debug("Detected provider from service field", { provider: data["service"] });
861
+ return data["service"];
862
+ }
863
+ if (data["status"] === "ok") {
864
+ logger.debug("Detected sub2api from status: ok pattern");
865
+ return "sub2api";
866
+ }
867
+ logger.debug("Health probe returned unrecognized pattern", { data });
868
+ return null;
869
+ } catch (error) {
870
+ logger.debug("Health probe failed", { error: String(error) });
871
+ return null;
872
+ }
873
+ }
874
+
778
875
  // src/providers/claude-relay-service.ts
779
876
  function computeWeeklyResetTime(resetDay, resetHour) {
780
877
  const now = new Date;
@@ -790,20 +887,19 @@ function computeWeeklyResetTime(resetDay, resetHour) {
790
887
  function createQuotaWindow2(used, limit, resetsAt) {
791
888
  if (used === undefined)
792
889
  return null;
793
- const actualLimit = limit && limit > 0 ? limit : null;
794
- let remaining = null;
795
- if (actualLimit !== null) {
796
- remaining = Math.max(0, actualLimit - used);
797
- }
890
+ if (!limit || limit <= 0)
891
+ return null;
892
+ const remaining = Math.max(0, limit - used);
798
893
  return {
799
894
  used,
800
- limit: actualLimit,
895
+ limit,
801
896
  remaining,
802
897
  resetsAt
803
898
  };
804
899
  }
805
900
  async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000) {
806
- const url = `${baseUrl}/apiStats/api/user-stats`;
901
+ const origin = extractOrigin(baseUrl);
902
+ const url = `${origin}/apiStats/api/user-stats`;
807
903
  const resolvedUA = resolveUserAgent(config.spoofClaudeCodeUA);
808
904
  if (resolvedUA) {
809
905
  logger.debug(`Using User-Agent: ${resolvedUA}`);
@@ -836,16 +932,18 @@ async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000)
836
932
  tokenStats: null,
837
933
  rateLimit: null
838
934
  };
839
- result.daily = createQuotaWindow2(limits.currentDailyCost, limits.dailyCostLimit, null);
935
+ result.daily = createQuotaWindow2(limits.currentDailyCost, limits.dailyCostLimit, computeNextMidnightLocal());
840
936
  if (limits.weeklyResetDay !== undefined && limits.weeklyResetHour !== undefined) {
841
937
  const weeklyResetsAt = computeWeeklyResetTime(limits.weeklyResetDay, limits.weeklyResetHour);
842
938
  result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, weeklyResetsAt);
843
939
  } else {
844
- result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, null);
940
+ result.weekly = createQuotaWindow2(limits.weeklyOpusCost, limits.weeklyOpusCostLimit, computeNextMondayLocal());
845
941
  }
846
942
  result.monthly = null;
847
943
  if (limits.windowEndTime) {
848
944
  result.resetsAt = new Date(limits.windowEndTime).toISOString();
945
+ } else {
946
+ result.resetsAt = computeSoonestReset(result);
849
947
  }
850
948
  if (data.usage?.total) {
851
949
  const total = data.usage.total;
@@ -1105,7 +1203,7 @@ async function fetchCustom(baseUrl, token, appConfig, providerConfig, timeoutMs
1105
1203
 
1106
1204
  // src/providers/autodetect.ts
1107
1205
  var detectionCache = new Map;
1108
- function detectProvider(baseUrl, customProviders = {}) {
1206
+ function detectProviderFromUrlPattern(baseUrl, customProviders = {}) {
1109
1207
  const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
1110
1208
  for (const [providerId, config] of Object.entries(customProviders)) {
1111
1209
  if (config.urlPatterns && config.urlPatterns.length > 0) {
@@ -1117,25 +1215,71 @@ function detectProvider(baseUrl, customProviders = {}) {
1117
1215
  }
1118
1216
  }
1119
1217
  }
1120
- if (normalizedUrl.includes("/apistats") || normalizedUrl.includes("relay") || normalizedUrl.includes("/api/user-stats")) {
1218
+ if (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats")) {
1121
1219
  return "claude-relay-service";
1122
1220
  }
1123
1221
  return "sub2api";
1124
1222
  }
1125
- function resolveProvider(baseUrl, providerOverride, customProviders = {}) {
1223
+ async function resolveProvider(baseUrl, providerOverride, customProviders = {}, probeTimeoutMs = 1500) {
1126
1224
  if (providerOverride) {
1225
+ logger.debug("Provider override detected", { provider: providerOverride });
1127
1226
  return providerOverride;
1128
1227
  }
1129
1228
  const cached = detectionCache.get(baseUrl);
1130
1229
  if (cached) {
1230
+ logger.debug("Provider detection cache hit (memory)", { provider: cached.provider });
1131
1231
  return cached.provider;
1132
1232
  }
1133
- const provider = detectProvider(baseUrl, customProviders);
1233
+ const diskCached = readProviderDetectionCache(baseUrl);
1234
+ if (diskCached) {
1235
+ logger.debug("Provider detection cache hit (disk)", {
1236
+ provider: diskCached.provider,
1237
+ detectedVia: diskCached.detectedVia
1238
+ });
1239
+ detectionCache.set(baseUrl, {
1240
+ provider: diskCached.provider,
1241
+ detectedAt: diskCached.detectedAt
1242
+ });
1243
+ return diskCached.provider;
1244
+ }
1245
+ for (const [providerId, config] of Object.entries(customProviders)) {
1246
+ if (config.urlPatterns && config.urlPatterns.length > 0) {
1247
+ const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
1248
+ for (const pattern of config.urlPatterns) {
1249
+ const normalizedPattern = pattern.toLowerCase();
1250
+ if (normalizedUrl.includes(normalizedPattern)) {
1251
+ logger.debug("Provider detected via custom URL pattern", { provider: providerId, pattern });
1252
+ cacheProviderDetection(baseUrl, providerId, "url-pattern");
1253
+ return providerId;
1254
+ }
1255
+ }
1256
+ }
1257
+ }
1258
+ logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
1259
+ const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
1260
+ if (probedProvider) {
1261
+ logger.debug("Provider detected via health probe", { provider: probedProvider });
1262
+ cacheProviderDetection(baseUrl, probedProvider, "health-probe");
1263
+ return probedProvider;
1264
+ }
1265
+ const patternProvider = detectProviderFromUrlPattern(baseUrl, {});
1266
+ logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
1267
+ cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
1268
+ return patternProvider;
1269
+ }
1270
+ function cacheProviderDetection(baseUrl, provider, detectedVia) {
1271
+ const now = new Date().toISOString();
1134
1272
  detectionCache.set(baseUrl, {
1135
1273
  provider,
1136
- detectedAt: new Date().toISOString()
1274
+ detectedAt: now
1275
+ });
1276
+ writeProviderDetectionCache(baseUrl, {
1277
+ baseUrl,
1278
+ provider,
1279
+ detectedVia,
1280
+ detectedAt: now,
1281
+ ttlSeconds: PROVIDER_DETECTION_TTL_SECONDS
1137
1282
  });
1138
- return provider;
1139
1283
  }
1140
1284
 
1141
1285
  // src/providers/index.ts
@@ -2086,7 +2230,7 @@ function uninstallStatusLine() {
2086
2230
  // package.json
2087
2231
  var package_default = {
2088
2232
  name: "cc-api-statusline",
2089
- version: "0.1.2",
2233
+ version: "0.1.4",
2090
2234
  description: "Claude Code statusline tool that polls API usage from third-party proxy backends",
2091
2235
  type: "module",
2092
2236
  bin: {
@@ -2292,9 +2436,10 @@ async function main() {
2292
2436
  const configPath = getConfigPath(args.configPath);
2293
2437
  const configHash = computeConfigHash(configPath);
2294
2438
  logger.debug("Config loaded", { configPath, configHash });
2295
- const providerId = resolveProvider(baseUrl, env.providerOverride, config.customProviders ?? {});
2439
+ const probeTimeout = isPiped ? Math.min(1500, Math.max(200, Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000) - 200)) : 3000;
2440
+ const providerId = await resolveProvider(baseUrl, env.providerOverride, config.customProviders ?? {}, probeTimeout);
2296
2441
  const provider = getProvider(providerId, config.customProviders ?? {});
2297
- logger.debug("Provider resolved", { providerId });
2442
+ logger.debug("Provider resolved", { providerId, probeTimeout });
2298
2443
  if (!provider) {
2299
2444
  logger.error("Provider not found", { providerId });
2300
2445
  const errorOutput = renderError("provider-unknown", "without-cache");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-api-statusline",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Claude Code statusline tool that polls API usage from third-party proxy backends",
5
5
  "type": "module",
6
6
  "bin": {