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.
- package/dist/cc-api-statusline.js +170 -25
- package/package.json +1 -1
|
@@ -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/
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
|
895
|
+
limit,
|
|
801
896
|
remaining,
|
|
802
897
|
resetsAt
|
|
803
898
|
};
|
|
804
899
|
}
|
|
805
900
|
async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = 5000) {
|
|
806
|
-
const
|
|
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,
|
|
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,
|
|
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
|
|
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("
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
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");
|