cc-api-statusline 0.2.1 → 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.
- package/README.md +126 -428
- package/dist/cc-api-statusline.js +1191 -613
- package/package.json +1 -1
|
@@ -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.
|
|
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: {
|
|
@@ -68,6 +68,7 @@ function parseArgs() {
|
|
|
68
68
|
let once = false;
|
|
69
69
|
let install = false;
|
|
70
70
|
let uninstall = false;
|
|
71
|
+
let applyConfig = false;
|
|
71
72
|
let force = false;
|
|
72
73
|
let configPath;
|
|
73
74
|
let runner;
|
|
@@ -83,6 +84,8 @@ function parseArgs() {
|
|
|
83
84
|
install = true;
|
|
84
85
|
} else if (arg === "--uninstall") {
|
|
85
86
|
uninstall = true;
|
|
87
|
+
} else if (arg === "--apply-config") {
|
|
88
|
+
applyConfig = true;
|
|
86
89
|
} else if (arg === "--force") {
|
|
87
90
|
force = true;
|
|
88
91
|
} else if (arg === "--config" && i + 1 < args.length) {
|
|
@@ -96,7 +99,7 @@ function parseArgs() {
|
|
|
96
99
|
i++;
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
|
-
return { help, version, once, install, uninstall, force, configPath, runner };
|
|
102
|
+
return { help, version, once, install, uninstall, applyConfig, force, configPath, runner };
|
|
100
103
|
}
|
|
101
104
|
function showHelp() {
|
|
102
105
|
console.log(`
|
|
@@ -110,8 +113,9 @@ Options:
|
|
|
110
113
|
--version, -v Show version
|
|
111
114
|
--once Fetch once and exit (no polling)
|
|
112
115
|
--config <path> Use custom config file
|
|
113
|
-
--install Register as Claude Code statusline widget
|
|
116
|
+
--install Register as Claude Code statusline widget and create default configs
|
|
114
117
|
--uninstall Remove statusline widget registration
|
|
118
|
+
--apply-config Apply endpoint config changes (updates lock file, clears caches)
|
|
115
119
|
--runner <runner> Package runner: npx or bunx (default: auto-detect)
|
|
116
120
|
--force Force overwrite existing statusline configuration
|
|
117
121
|
|
|
@@ -134,13 +138,13 @@ function showVersion() {
|
|
|
134
138
|
console.log(`cc-api-statusline v${package_default.version}`);
|
|
135
139
|
}
|
|
136
140
|
// src/services/settings.ts
|
|
137
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
141
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
138
142
|
import { execSync } from "child_process";
|
|
139
143
|
|
|
140
144
|
// src/services/env.ts
|
|
141
|
-
import { readFileSync
|
|
142
|
-
import { join } from "path";
|
|
143
|
-
import { homedir } from "os";
|
|
145
|
+
import { readFileSync } from "fs";
|
|
146
|
+
import { join as join4 } from "path";
|
|
147
|
+
import { homedir as homedir2 } from "os";
|
|
144
148
|
|
|
145
149
|
// src/services/hash.ts
|
|
146
150
|
function sha256(input) {
|
|
@@ -157,21 +161,193 @@ function shortHash(input, length = 12) {
|
|
|
157
161
|
return fullHash.slice(0, length);
|
|
158
162
|
}
|
|
159
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
|
+
|
|
160
331
|
// src/services/env.ts
|
|
161
332
|
function getSettingsJsonPath() {
|
|
162
333
|
const configDir = process.env["CLAUDE_CONFIG_DIR"];
|
|
163
334
|
if (configDir) {
|
|
164
|
-
return
|
|
335
|
+
return join4(configDir, "settings.json");
|
|
165
336
|
}
|
|
166
|
-
return
|
|
337
|
+
return join4(homedir2(), ".claude", "settings.json");
|
|
167
338
|
}
|
|
168
339
|
function readSettingsJsonEnv() {
|
|
169
340
|
const settingsPath = getSettingsJsonPath();
|
|
170
|
-
|
|
171
|
-
|
|
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;
|
|
172
349
|
}
|
|
173
350
|
try {
|
|
174
|
-
const content = readFileSync(settingsPath, "utf-8");
|
|
175
351
|
const settings = JSON.parse(content);
|
|
176
352
|
if (settings["env"] && typeof settings["env"] === "object") {
|
|
177
353
|
const env = settings["env"];
|
|
@@ -186,8 +362,8 @@ function readSettingsJsonEnv() {
|
|
|
186
362
|
return result;
|
|
187
363
|
}
|
|
188
364
|
return {};
|
|
189
|
-
} catch (
|
|
190
|
-
|
|
365
|
+
} catch (err) {
|
|
366
|
+
logger.warn(`Could not read settings.json: ${err}`);
|
|
191
367
|
return {};
|
|
192
368
|
}
|
|
193
369
|
}
|
|
@@ -227,25 +403,15 @@ function validateRequiredEnv(env) {
|
|
|
227
403
|
}
|
|
228
404
|
|
|
229
405
|
// src/services/atomic-write.ts
|
|
230
|
-
import { writeFileSync, renameSync, unlinkSync
|
|
231
|
-
import { dirname } from "path";
|
|
232
|
-
|
|
233
|
-
// src/services/ensure-dir.ts
|
|
234
|
-
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
235
|
-
function ensureDir(dirPath) {
|
|
236
|
-
if (!existsSync2(dirPath)) {
|
|
237
|
-
mkdirSync(dirPath, { recursive: true, mode: 448 });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// src/services/atomic-write.ts
|
|
406
|
+
import { writeFileSync, renameSync as renameSync2, unlinkSync as unlinkSync2, existsSync, chmodSync } from "fs";
|
|
407
|
+
import { dirname as dirname3 } from "path";
|
|
242
408
|
function atomicWriteFile(filePath, content, opts = {}) {
|
|
243
409
|
const { mode = 384, ensureParentDir: ensureParent = false, appendNewline = false } = opts;
|
|
244
410
|
const nonce = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
245
411
|
const tmpPath = `${filePath}.${nonce}.tmp`;
|
|
246
412
|
try {
|
|
247
413
|
if (ensureParent) {
|
|
248
|
-
const dir =
|
|
414
|
+
const dir = dirname3(filePath);
|
|
249
415
|
ensureDir(dir);
|
|
250
416
|
}
|
|
251
417
|
const finalContent = appendNewline ? `${content}
|
|
@@ -254,11 +420,11 @@ function atomicWriteFile(filePath, content, opts = {}) {
|
|
|
254
420
|
try {
|
|
255
421
|
chmodSync(tmpPath, mode);
|
|
256
422
|
} catch {}
|
|
257
|
-
|
|
423
|
+
renameSync2(tmpPath, filePath);
|
|
258
424
|
} catch (error) {
|
|
259
425
|
try {
|
|
260
|
-
if (
|
|
261
|
-
|
|
426
|
+
if (existsSync(tmpPath)) {
|
|
427
|
+
unlinkSync2(tmpPath);
|
|
262
428
|
}
|
|
263
429
|
} catch {}
|
|
264
430
|
throw new Error(`Failed to write file atomically: ${error}`);
|
|
@@ -268,14 +434,14 @@ function atomicWriteFile(filePath, content, opts = {}) {
|
|
|
268
434
|
// src/services/settings.ts
|
|
269
435
|
function loadClaudeSettings() {
|
|
270
436
|
const path = getSettingsJsonPath();
|
|
271
|
-
if (!
|
|
437
|
+
if (!existsSync2(path)) {
|
|
272
438
|
return {};
|
|
273
439
|
}
|
|
274
440
|
try {
|
|
275
441
|
const content = readFileSync2(path, "utf-8");
|
|
276
442
|
return JSON.parse(content);
|
|
277
443
|
} catch (error) {
|
|
278
|
-
|
|
444
|
+
logger.warn(`Failed to read settings from ${path}: ${error}`);
|
|
279
445
|
return {};
|
|
280
446
|
}
|
|
281
447
|
}
|
|
@@ -321,38 +487,9 @@ function uninstallStatusLine() {
|
|
|
321
487
|
}
|
|
322
488
|
}
|
|
323
489
|
|
|
324
|
-
// src/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (existing && !args.force) {
|
|
328
|
-
console.error("Error: statusLine is already configured in settings.json");
|
|
329
|
-
console.error(`Current command: ${existing}`);
|
|
330
|
-
console.error("Use --force to overwrite, or --uninstall to remove first.");
|
|
331
|
-
process.exit(1);
|
|
332
|
-
}
|
|
333
|
-
const runner = args.runner ?? (isBunxAvailable() ? "bunx" : "npx");
|
|
334
|
-
installStatusLine(runner);
|
|
335
|
-
console.log("✓ Statusline installed successfully!");
|
|
336
|
-
console.log(` Runner: ${runner}`);
|
|
337
|
-
console.log(` Command: ${runner} -y cc-api-statusline@latest`);
|
|
338
|
-
console.log(` Config: ~/.claude/settings.json`);
|
|
339
|
-
process.exit(0);
|
|
340
|
-
}
|
|
341
|
-
function handleUninstall() {
|
|
342
|
-
const existing = getExistingStatusLine();
|
|
343
|
-
if (!existing) {
|
|
344
|
-
console.log("No statusLine configuration found in settings.json");
|
|
345
|
-
process.exit(0);
|
|
346
|
-
}
|
|
347
|
-
uninstallStatusLine();
|
|
348
|
-
console.log("✓ Statusline uninstalled successfully");
|
|
349
|
-
console.log(" Removed statusLine from ~/.claude/settings.json");
|
|
350
|
-
process.exit(0);
|
|
351
|
-
}
|
|
352
|
-
// src/services/cache.ts
|
|
353
|
-
import { readFileSync as readFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
354
|
-
import { join as join2 } from "path";
|
|
355
|
-
import { homedir as homedir2 } from "os";
|
|
490
|
+
// src/services/config-defaults.ts
|
|
491
|
+
import { join as join8 } from "path";
|
|
492
|
+
import { existsSync as existsSync4 } from "fs";
|
|
356
493
|
|
|
357
494
|
// src/types/normalized-usage.ts
|
|
358
495
|
function createEmptyNormalizedUsage(provider, billingMode, planName) {
|
|
@@ -385,6 +522,14 @@ function computeSoonestReset(usage) {
|
|
|
385
522
|
return sorted[0] ?? null;
|
|
386
523
|
}
|
|
387
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
|
+
}
|
|
388
533
|
var DEFAULT_CONFIG = {
|
|
389
534
|
display: {
|
|
390
535
|
layout: "standard",
|
|
@@ -392,7 +537,7 @@ var DEFAULT_CONFIG = {
|
|
|
392
537
|
progressStyle: "icon",
|
|
393
538
|
barSize: "medium",
|
|
394
539
|
barStyle: "block",
|
|
395
|
-
|
|
540
|
+
divider: DEFAULT_DIVIDER_CONFIG,
|
|
396
541
|
maxWidth: 100,
|
|
397
542
|
clockFormat: "24h",
|
|
398
543
|
colorMode: "auto",
|
|
@@ -405,28 +550,17 @@ var DEFAULT_CONFIG = {
|
|
|
405
550
|
balance: true,
|
|
406
551
|
tokens: false,
|
|
407
552
|
rateLimit: false,
|
|
408
|
-
plan: false
|
|
409
|
-
divider: true
|
|
553
|
+
plan: false
|
|
410
554
|
},
|
|
411
555
|
colors: {
|
|
412
|
-
auto: {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
]
|
|
420
|
-
},
|
|
421
|
-
chill: {
|
|
422
|
-
low: "cyan",
|
|
423
|
-
medium: "blue",
|
|
424
|
-
high: "magenta",
|
|
425
|
-
lowThreshold: 50,
|
|
426
|
-
highThreshold: 80
|
|
427
|
-
}
|
|
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"]) }
|
|
428
563
|
},
|
|
429
|
-
customProviders: {},
|
|
430
564
|
pollIntervalSeconds: 30,
|
|
431
565
|
pipedRequestTimeoutMs: 800
|
|
432
566
|
};
|
|
@@ -493,12 +627,12 @@ var DEFAULT_COMPONENT_ORDER = [
|
|
|
493
627
|
"plan"
|
|
494
628
|
];
|
|
495
629
|
// src/types/cache.ts
|
|
496
|
-
var CACHE_VERSION =
|
|
630
|
+
var CACHE_VERSION = 2;
|
|
497
631
|
function isCacheEntry(value) {
|
|
498
632
|
if (typeof value !== "object" || value === null)
|
|
499
633
|
return false;
|
|
500
634
|
const c = value;
|
|
501
|
-
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");
|
|
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");
|
|
502
636
|
}
|
|
503
637
|
var PROVIDER_DETECTION_TTL_SECONDS = 86400;
|
|
504
638
|
function isProviderDetectionCacheEntry(value) {
|
|
@@ -507,150 +641,260 @@ function isProviderDetectionCacheEntry(value) {
|
|
|
507
641
|
const c = value;
|
|
508
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";
|
|
509
643
|
}
|
|
510
|
-
// src/services/
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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";
|
|
647
|
+
function getEndpointConfigDir(customRoot) {
|
|
648
|
+
const envRoot = process.env["CC_API_STATUSLINE_CONFIG_DIR"];
|
|
649
|
+
const root = customRoot || envRoot || getConfigDir();
|
|
650
|
+
return join5(root, "api-config");
|
|
651
|
+
}
|
|
652
|
+
function loadEndpointConfigs(customDir) {
|
|
653
|
+
const configDir = getEndpointConfigDir(customDir);
|
|
654
|
+
if (!existsSync3(configDir)) {
|
|
655
|
+
return getBuiltInEndpointConfigs();
|
|
656
|
+
}
|
|
657
|
+
const registry = {};
|
|
658
|
+
const files = readdirSync2(configDir).filter((f) => f.endsWith(".json"));
|
|
659
|
+
if (files.length === 0) {
|
|
660
|
+
return getBuiltInEndpointConfigs();
|
|
661
|
+
}
|
|
662
|
+
for (const file of files) {
|
|
663
|
+
const filePath = join5(configDir, file);
|
|
664
|
+
try {
|
|
665
|
+
const config = loadEndpointConfigFile(filePath);
|
|
666
|
+
registry[config.provider] = config;
|
|
667
|
+
} catch (error) {
|
|
668
|
+
logger.error(`Failed to load endpoint config ${file}`, { error: String(error) });
|
|
669
|
+
}
|
|
515
670
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
ensureDir(dir);
|
|
671
|
+
if (Object.keys(registry).length === 0) {
|
|
672
|
+
return getBuiltInEndpointConfigs();
|
|
673
|
+
}
|
|
674
|
+
return registry;
|
|
521
675
|
}
|
|
522
|
-
function
|
|
523
|
-
const
|
|
524
|
-
|
|
676
|
+
function loadEndpointConfigFile(filePath) {
|
|
677
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
678
|
+
const data = JSON.parse(content);
|
|
679
|
+
validateEndpointConfig(data, filePath);
|
|
680
|
+
return data;
|
|
525
681
|
}
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return null;
|
|
682
|
+
function validateEndpointConfig(data, filename) {
|
|
683
|
+
if (typeof data !== "object" || data === null) {
|
|
684
|
+
throw new Error(`${filename}: Config must be an object`);
|
|
530
685
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (!isCacheEntry(data)) {
|
|
535
|
-
console.warn(`Invalid cache structure at ${path}`);
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
return data;
|
|
539
|
-
} catch (error) {
|
|
540
|
-
console.warn(`Failed to read cache from ${path}: ${error}`);
|
|
541
|
-
return null;
|
|
686
|
+
const config = data;
|
|
687
|
+
if (typeof config.provider !== "string" || !config.provider) {
|
|
688
|
+
throw new Error(`${filename}: Missing or invalid 'provider' field`);
|
|
542
689
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const path = getCachePath(baseUrl);
|
|
546
|
-
try {
|
|
547
|
-
ensureCacheDir();
|
|
548
|
-
const content = JSON.stringify(entry, null, 2);
|
|
549
|
-
atomicWriteFile(path, content);
|
|
550
|
-
} catch (error) {
|
|
551
|
-
console.warn(`Failed to write cache to ${path}: ${error}`);
|
|
690
|
+
if (typeof config.endpoint !== "object" || config.endpoint === null) {
|
|
691
|
+
throw new Error(`${filename}: Missing or invalid 'endpoint' field`);
|
|
552
692
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const now = Date.now();
|
|
557
|
-
const age = now - fetchedAt;
|
|
558
|
-
const ttlMs = entry.ttlSeconds * 1000;
|
|
559
|
-
if (age >= ttlMs) {
|
|
560
|
-
return false;
|
|
693
|
+
const endpoint = config.endpoint;
|
|
694
|
+
if (typeof endpoint.path !== "string" || !endpoint.path) {
|
|
695
|
+
throw new Error(`${filename}: Missing or invalid 'endpoint.path' field`);
|
|
561
696
|
}
|
|
562
|
-
if (
|
|
563
|
-
|
|
697
|
+
if (endpoint.method !== "GET" && endpoint.method !== "POST") {
|
|
698
|
+
throw new Error(`${filename}: Invalid 'endpoint.method' (must be GET or POST)`);
|
|
564
699
|
}
|
|
565
|
-
if (
|
|
566
|
-
|
|
700
|
+
if (typeof config.auth !== "object" || config.auth === null) {
|
|
701
|
+
throw new Error(`${filename}: Missing or invalid 'auth' field`);
|
|
567
702
|
}
|
|
568
|
-
|
|
569
|
-
|
|
703
|
+
const auth = config.auth;
|
|
704
|
+
if (!["bearer-header", "body-key", "custom-header"].includes(auth.type)) {
|
|
705
|
+
throw new Error(`${filename}: Invalid 'auth.type' (must be bearer-header, body-key, or custom-header)`);
|
|
570
706
|
}
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
function isCacheProviderValid(entry, currentProvider) {
|
|
574
|
-
return entry.provider === currentProvider;
|
|
575
|
-
}
|
|
576
|
-
function isCacheRenderedLineUsable(entry, currentConfigHash) {
|
|
577
|
-
return entry.configHash === currentConfigHash;
|
|
578
|
-
}
|
|
579
|
-
function computeConfigHash(configPath) {
|
|
580
|
-
if (!existsSync5(configPath)) {
|
|
581
|
-
return sha256("").slice(0, 12);
|
|
707
|
+
if (typeof config.responseMapping !== "object" || config.responseMapping === null) {
|
|
708
|
+
throw new Error(`${filename}: Missing or invalid 'responseMapping' field`);
|
|
582
709
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
710
|
+
const mapping = config.responseMapping;
|
|
711
|
+
for (const [key, val] of Object.entries(mapping)) {
|
|
712
|
+
if (val !== undefined && typeof val !== "string") {
|
|
713
|
+
throw new Error(`${filename}: responseMapping.${key} must be a string`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (config.displayName !== undefined && typeof config.displayName !== "string") {
|
|
717
|
+
throw new Error(`${filename}: Invalid 'displayName' field (must be string)`);
|
|
718
|
+
}
|
|
719
|
+
if (config.defaults !== undefined && typeof config.defaults !== "object") {
|
|
720
|
+
throw new Error(`${filename}: Invalid 'defaults' field (must be object)`);
|
|
721
|
+
}
|
|
722
|
+
if (config.detection !== undefined && typeof config.detection !== "object") {
|
|
723
|
+
throw new Error(`${filename}: Invalid 'detection' field (must be object)`);
|
|
589
724
|
}
|
|
590
725
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (
|
|
594
|
-
|
|
726
|
+
function computeEndpointConfigHash(customDir) {
|
|
727
|
+
const configDir = getEndpointConfigDir(customDir);
|
|
728
|
+
if (!existsSync3(configDir)) {
|
|
729
|
+
const builtIn = getBuiltInEndpointConfigs();
|
|
730
|
+
const serialized = JSON.stringify(builtIn, Object.keys(builtIn).sort());
|
|
731
|
+
return shortHash(serialized, 12);
|
|
595
732
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
733
|
+
const files = readdirSync2(configDir).filter((f) => f.endsWith(".json")).sort();
|
|
734
|
+
if (files.length === 0) {
|
|
735
|
+
const builtIn = getBuiltInEndpointConfigs();
|
|
736
|
+
const serialized = JSON.stringify(builtIn, Object.keys(builtIn).sort());
|
|
737
|
+
return shortHash(serialized, 12);
|
|
738
|
+
}
|
|
739
|
+
let combined = "";
|
|
740
|
+
for (const file of files) {
|
|
741
|
+
const filePath = join5(configDir, file);
|
|
742
|
+
try {
|
|
743
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
744
|
+
combined += `\x00${file}\x00${content}`;
|
|
745
|
+
} catch {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return shortHash(combined, 12);
|
|
598
750
|
}
|
|
599
|
-
function
|
|
600
|
-
|
|
601
|
-
|
|
751
|
+
function getBuiltInEndpointConfigs() {
|
|
752
|
+
return {
|
|
753
|
+
sub2api: {
|
|
754
|
+
provider: "sub2api",
|
|
755
|
+
displayName: "sub2api",
|
|
756
|
+
endpoint: {
|
|
757
|
+
path: "/v1/usage",
|
|
758
|
+
method: "GET"
|
|
759
|
+
},
|
|
760
|
+
auth: {
|
|
761
|
+
type: "bearer-header"
|
|
762
|
+
},
|
|
763
|
+
defaults: {
|
|
764
|
+
unit: "USD",
|
|
765
|
+
planName: "Unknown"
|
|
766
|
+
},
|
|
767
|
+
detection: {
|
|
768
|
+
healthMatch: { status: "ok" }
|
|
769
|
+
},
|
|
770
|
+
responseMapping: {
|
|
771
|
+
billingMode: "subscription",
|
|
772
|
+
planName: "$.planName",
|
|
773
|
+
"balance.remaining": "$.remaining",
|
|
774
|
+
"balance.unit": "$.unit",
|
|
775
|
+
"daily.used": "$.subscription.daily_usage_usd",
|
|
776
|
+
"daily.limit": "$.subscription.daily_limit_usd",
|
|
777
|
+
"weekly.used": "$.subscription.weekly_usage_usd",
|
|
778
|
+
"weekly.limit": "$.subscription.weekly_limit_usd",
|
|
779
|
+
"monthly.used": "$.subscription.monthly_usage_usd",
|
|
780
|
+
"monthly.limit": "$.subscription.monthly_limit_usd",
|
|
781
|
+
"tokenStats.today.requests": "$.usage.today.requests",
|
|
782
|
+
"tokenStats.today.inputTokens": "$.usage.today.input_tokens",
|
|
783
|
+
"tokenStats.today.outputTokens": "$.usage.today.output_tokens",
|
|
784
|
+
"tokenStats.today.cacheCreationTokens": "$.usage.today.cache_creation_tokens",
|
|
785
|
+
"tokenStats.today.cacheReadTokens": "$.usage.today.cache_read_tokens",
|
|
786
|
+
"tokenStats.today.totalTokens": "$.usage.today.total_tokens",
|
|
787
|
+
"tokenStats.today.cost": "$.usage.today.cost",
|
|
788
|
+
"tokenStats.total.requests": "$.usage.total.requests",
|
|
789
|
+
"tokenStats.total.inputTokens": "$.usage.total.input_tokens",
|
|
790
|
+
"tokenStats.total.outputTokens": "$.usage.total.output_tokens",
|
|
791
|
+
"tokenStats.total.totalTokens": "$.usage.total.total_tokens",
|
|
792
|
+
"tokenStats.total.cost": "$.usage.total.cost",
|
|
793
|
+
"tokenStats.rpm": "$.usage.rpm",
|
|
794
|
+
"tokenStats.tpm": "$.usage.tpm"
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
"claude-relay-service": {
|
|
798
|
+
provider: "claude-relay-service",
|
|
799
|
+
displayName: "CRS",
|
|
800
|
+
endpoint: {
|
|
801
|
+
path: "/apiStats/api/user-stats",
|
|
802
|
+
method: "POST",
|
|
803
|
+
contentType: "application/json"
|
|
804
|
+
},
|
|
805
|
+
auth: {
|
|
806
|
+
type: "body-key",
|
|
807
|
+
bodyField: "apiKey"
|
|
808
|
+
},
|
|
809
|
+
defaults: {
|
|
810
|
+
billingMode: "subscription",
|
|
811
|
+
planName: "API Key",
|
|
812
|
+
resetSemantics: "rolling-window"
|
|
813
|
+
},
|
|
814
|
+
detection: {
|
|
815
|
+
urlPatterns: ["/apistats", "/api/user-stats"],
|
|
816
|
+
healthMatch: { service: "*" }
|
|
817
|
+
},
|
|
818
|
+
responseMapping: {
|
|
819
|
+
billingMode: "subscription",
|
|
820
|
+
planName: "$.data.name",
|
|
821
|
+
"daily.used": "$.data.limits.currentDailyCost",
|
|
822
|
+
"daily.limit": "$.data.limits.dailyCostLimit",
|
|
823
|
+
"weekly.used": "$.data.limits.weeklyOpusCost",
|
|
824
|
+
"weekly.limit": "$.data.limits.weeklyOpusCostLimit",
|
|
825
|
+
"monthly.used": "$.data.limits.currentTotalCost",
|
|
826
|
+
"monthly.limit": "$.data.limits.totalCostLimit",
|
|
827
|
+
"tokenStats.total.requests": "$.data.usage.total.requests",
|
|
828
|
+
"tokenStats.total.inputTokens": "$.data.usage.total.inputTokens",
|
|
829
|
+
"tokenStats.total.outputTokens": "$.data.usage.total.outputTokens",
|
|
830
|
+
"tokenStats.total.cacheCreationTokens": "$.data.usage.total.cacheCreateTokens",
|
|
831
|
+
"tokenStats.total.cacheReadTokens": "$.data.usage.total.cacheReadTokens",
|
|
832
|
+
"tokenStats.total.totalTokens": "$.data.usage.total.tokens",
|
|
833
|
+
"tokenStats.total.cost": "$.data.usage.total.cost",
|
|
834
|
+
"rateLimit.windowSeconds": "$.data.limits.rateLimitWindow",
|
|
835
|
+
"rateLimit.requestsUsed": "$.data.limits.currentWindowRequests",
|
|
836
|
+
"rateLimit.requestsLimit": "$.data.limits.rateLimitRequests",
|
|
837
|
+
"rateLimit.costUsed": "$.data.limits.currentWindowCost",
|
|
838
|
+
"rateLimit.costLimit": "$.data.limits.rateLimitCost",
|
|
839
|
+
"rateLimit.remainingSeconds": "$.data.limits.windowRemainingSeconds"
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
};
|
|
602
843
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
844
|
+
|
|
845
|
+
// src/services/endpoint-lock.ts
|
|
846
|
+
import { readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
847
|
+
import { join as join6 } from "path";
|
|
848
|
+
function getLockFilePath(customDir) {
|
|
849
|
+
if (customDir) {
|
|
850
|
+
return join6(customDir, ".endpoint-config.lock");
|
|
607
851
|
}
|
|
852
|
+
return join6(getConfigDir(), ".endpoint-config.lock");
|
|
853
|
+
}
|
|
854
|
+
function readEndpointLock(customDir) {
|
|
855
|
+
const lockPath = getLockFilePath(customDir);
|
|
856
|
+
let content;
|
|
608
857
|
try {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (
|
|
612
|
-
console.warn(`Invalid provider detection cache structure at ${path}`);
|
|
858
|
+
content = readFileSync4(lockPath, "utf-8");
|
|
859
|
+
} catch (err) {
|
|
860
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
613
861
|
return null;
|
|
614
862
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
863
|
+
throw err;
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const data = JSON.parse(content);
|
|
867
|
+
if (typeof data === "object" && data !== null && "hash" in data && "lockedAt" in data && typeof data.hash === "string" && typeof data.lockedAt === "string") {
|
|
868
|
+
return {
|
|
869
|
+
hash: data.hash,
|
|
870
|
+
lockedAt: data.lockedAt
|
|
871
|
+
};
|
|
624
872
|
}
|
|
625
|
-
return
|
|
626
|
-
} catch
|
|
627
|
-
console.warn(`Failed to read provider detection cache from ${path}: ${error}`);
|
|
873
|
+
return null;
|
|
874
|
+
} catch {
|
|
628
875
|
return null;
|
|
629
876
|
}
|
|
630
877
|
}
|
|
631
|
-
function
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
878
|
+
function writeEndpointLock(hash, customDir) {
|
|
879
|
+
const lockPath = getLockFilePath(customDir);
|
|
880
|
+
const entry = {
|
|
881
|
+
hash,
|
|
882
|
+
lockedAt: new Date().toISOString()
|
|
883
|
+
};
|
|
884
|
+
atomicWriteFile(lockPath, JSON.stringify(entry, null, 2), {
|
|
885
|
+
ensureParentDir: true,
|
|
886
|
+
appendNewline: true
|
|
887
|
+
});
|
|
640
888
|
}
|
|
641
889
|
|
|
642
890
|
// src/services/config.ts
|
|
643
|
-
import { readFileSync as
|
|
644
|
-
import { join as
|
|
645
|
-
import { homedir as homedir3 } from "os";
|
|
646
|
-
function getConfigDir() {
|
|
647
|
-
return join3(homedir3(), ".claude", "cc-api-statusline");
|
|
648
|
-
}
|
|
891
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
892
|
+
import { join as join7 } from "path";
|
|
649
893
|
function getConfigPath(customPath) {
|
|
650
894
|
if (customPath) {
|
|
651
895
|
return customPath;
|
|
652
896
|
}
|
|
653
|
-
return
|
|
897
|
+
return join7(getConfigDir(), "config.json");
|
|
654
898
|
}
|
|
655
899
|
function deepMerge(target, source) {
|
|
656
900
|
const result = { ...target };
|
|
@@ -670,19 +914,19 @@ function validateConfig(config) {
|
|
|
670
914
|
let pollIntervalSeconds = config.pollIntervalSeconds;
|
|
671
915
|
let pipedRequestTimeoutMs = config.pipedRequestTimeoutMs;
|
|
672
916
|
if (maxWidth < 20) {
|
|
673
|
-
|
|
917
|
+
logger.warn("display.maxWidth < 20, clamping to 20");
|
|
674
918
|
maxWidth = 20;
|
|
675
919
|
}
|
|
676
920
|
if (maxWidth > 100) {
|
|
677
|
-
|
|
921
|
+
logger.warn("display.maxWidth > 100, clamping to 100");
|
|
678
922
|
maxWidth = 100;
|
|
679
923
|
}
|
|
680
924
|
if (pollIntervalSeconds !== undefined && pollIntervalSeconds < 5) {
|
|
681
|
-
|
|
925
|
+
logger.warn("pollIntervalSeconds < 5, clamping to 5");
|
|
682
926
|
pollIntervalSeconds = 5;
|
|
683
927
|
}
|
|
684
928
|
if (pipedRequestTimeoutMs !== undefined && pipedRequestTimeoutMs < 100) {
|
|
685
|
-
|
|
929
|
+
logger.warn("pipedRequestTimeoutMs < 100, clamping to 100");
|
|
686
930
|
pipedRequestTimeoutMs = 100;
|
|
687
931
|
}
|
|
688
932
|
return {
|
|
@@ -695,22 +939,91 @@ function validateConfig(config) {
|
|
|
695
939
|
pipedRequestTimeoutMs
|
|
696
940
|
};
|
|
697
941
|
}
|
|
698
|
-
function
|
|
699
|
-
const path = getConfigPath(configPath);
|
|
700
|
-
if (!existsSync6(path)) {
|
|
701
|
-
return DEFAULT_CONFIG;
|
|
702
|
-
}
|
|
942
|
+
function parseConfigContent(content, path) {
|
|
703
943
|
try {
|
|
704
|
-
const content = readFileSync4(path, "utf-8");
|
|
705
944
|
const userConfig = JSON.parse(content);
|
|
706
945
|
const merged = deepMerge(DEFAULT_CONFIG, userConfig);
|
|
707
946
|
return validateConfig(merged);
|
|
708
|
-
} catch (
|
|
709
|
-
|
|
710
|
-
|
|
947
|
+
} catch (err) {
|
|
948
|
+
logger.warn(`Could not load config from ${path}: ${err}`);
|
|
949
|
+
logger.warn("Using default configuration");
|
|
711
950
|
return DEFAULT_CONFIG;
|
|
712
951
|
}
|
|
713
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
|
+
|
|
974
|
+
// src/services/config-defaults.ts
|
|
975
|
+
function getDefaultStyleConfig() {
|
|
976
|
+
return DEFAULT_CONFIG;
|
|
977
|
+
}
|
|
978
|
+
function getDefaultSub2apiConfig() {
|
|
979
|
+
const configs = getBuiltInEndpointConfigs();
|
|
980
|
+
const config = configs["sub2api"];
|
|
981
|
+
if (!config)
|
|
982
|
+
throw new Error("Built-in sub2api config not found");
|
|
983
|
+
return config;
|
|
984
|
+
}
|
|
985
|
+
function getDefaultCrsConfig() {
|
|
986
|
+
const configs = getBuiltInEndpointConfigs();
|
|
987
|
+
const config = configs["claude-relay-service"];
|
|
988
|
+
if (!config)
|
|
989
|
+
throw new Error("Built-in claude-relay-service config not found");
|
|
990
|
+
return config;
|
|
991
|
+
}
|
|
992
|
+
function writeDefaultConfigs(customDir) {
|
|
993
|
+
const configDir = customDir || getConfigDir();
|
|
994
|
+
const configPath = join8(configDir, "config.json");
|
|
995
|
+
const apiConfigDir = getEndpointConfigDir(customDir);
|
|
996
|
+
ensureDir(configDir);
|
|
997
|
+
ensureDir(apiConfigDir);
|
|
998
|
+
if (!existsSync4(configPath)) {
|
|
999
|
+
const styleConfigWithoutColors = serializableConfig(getDefaultStyleConfig());
|
|
1000
|
+
atomicWriteFile(configPath, JSON.stringify(styleConfigWithoutColors, null, 2), {
|
|
1001
|
+
appendNewline: true
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
const sub2apiPath = join8(apiConfigDir, "sub2api.json");
|
|
1005
|
+
if (!existsSync4(sub2apiPath)) {
|
|
1006
|
+
const sub2apiConfig = getDefaultSub2apiConfig();
|
|
1007
|
+
atomicWriteFile(sub2apiPath, JSON.stringify(sub2apiConfig, null, 2), {
|
|
1008
|
+
appendNewline: true
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
const crsPath = join8(apiConfigDir, "crs.json");
|
|
1012
|
+
if (!existsSync4(crsPath)) {
|
|
1013
|
+
const crsConfig = getDefaultCrsConfig();
|
|
1014
|
+
atomicWriteFile(crsPath, JSON.stringify(crsConfig, null, 2), {
|
|
1015
|
+
appendNewline: true
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
const currentHash = computeEndpointConfigHash(customDir);
|
|
1019
|
+
writeEndpointLock(currentHash, customDir);
|
|
1020
|
+
}
|
|
1021
|
+
function needsConfigInit(customDir) {
|
|
1022
|
+
const configDir = customDir || getConfigDir();
|
|
1023
|
+
const configPath = join8(configDir, "config.json");
|
|
1024
|
+
const apiConfigDir = getEndpointConfigDir(customDir);
|
|
1025
|
+
return !existsSync4(configPath) || !existsSync4(apiConfigDir);
|
|
1026
|
+
}
|
|
714
1027
|
|
|
715
1028
|
// src/providers/http.ts
|
|
716
1029
|
class HttpError extends Error {
|
|
@@ -816,76 +1129,345 @@ async function secureFetch(url, options = {}, timeoutMs = 5000, userAgent) {
|
|
|
816
1129
|
}
|
|
817
1130
|
}
|
|
818
1131
|
|
|
819
|
-
// src/
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1132
|
+
// src/providers/health-probe.ts
|
|
1133
|
+
function extractOrigin(baseUrl) {
|
|
1134
|
+
try {
|
|
1135
|
+
const url = new URL(baseUrl);
|
|
1136
|
+
return url.origin;
|
|
1137
|
+
} catch {
|
|
1138
|
+
return baseUrl;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
1142
|
+
const origin = extractOrigin(baseUrl);
|
|
1143
|
+
const healthUrl = `${origin}/health`;
|
|
1144
|
+
logger.debug("Probing health endpoint", { healthUrl, timeoutMs });
|
|
1145
|
+
try {
|
|
1146
|
+
const responseText = await secureFetch(healthUrl, {
|
|
1147
|
+
method: "GET",
|
|
1148
|
+
headers: {
|
|
1149
|
+
Accept: "application/json"
|
|
1150
|
+
}
|
|
1151
|
+
}, timeoutMs);
|
|
1152
|
+
const data = JSON.parse(responseText);
|
|
1153
|
+
logger.debug("Health probe response", { data });
|
|
1154
|
+
if (typeof data["service"] === "string") {
|
|
1155
|
+
logger.debug("Detected provider from service field", { provider: data["service"] });
|
|
1156
|
+
return data["service"];
|
|
1157
|
+
}
|
|
1158
|
+
if (data["status"] === "ok") {
|
|
1159
|
+
logger.debug("Detected sub2api from status: ok pattern");
|
|
1160
|
+
return "sub2api";
|
|
1161
|
+
}
|
|
1162
|
+
logger.debug("Health probe returned unrecognized pattern", { data });
|
|
1163
|
+
return null;
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
logger.debug("Health probe failed", { error: String(error) });
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
823
1169
|
|
|
824
|
-
// src/services/
|
|
825
|
-
import {
|
|
826
|
-
import { join as
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1170
|
+
// src/services/cache.ts
|
|
1171
|
+
import { readFileSync as readFileSync6, unlinkSync as unlinkSync4 } from "fs";
|
|
1172
|
+
import { join as join9 } from "path";
|
|
1173
|
+
function getCacheDir() {
|
|
1174
|
+
const override = process.env["CC_API_STATUSLINE_CACHE_DIR"];
|
|
1175
|
+
if (override) {
|
|
1176
|
+
return override;
|
|
1177
|
+
}
|
|
1178
|
+
return getConfigDir();
|
|
1179
|
+
}
|
|
1180
|
+
function ensureCacheDir() {
|
|
1181
|
+
const dir = getCacheDir();
|
|
1182
|
+
ensureDir(dir);
|
|
1183
|
+
}
|
|
1184
|
+
function getCachePath(baseUrl) {
|
|
1185
|
+
const hash = shortHash(baseUrl, 12);
|
|
1186
|
+
return join9(getCacheDir(), `cache-${hash}.json`);
|
|
1187
|
+
}
|
|
1188
|
+
function readCache(baseUrl) {
|
|
1189
|
+
const path = getCachePath(baseUrl);
|
|
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;
|
|
837
1196
|
}
|
|
1197
|
+
throw err;
|
|
838
1198
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
this.enabled = false;
|
|
1199
|
+
try {
|
|
1200
|
+
const data = JSON.parse(content);
|
|
1201
|
+
if (!isCacheEntry(data)) {
|
|
1202
|
+
logger.warn(`Invalid cache structure at ${path}`);
|
|
1203
|
+
return null;
|
|
845
1204
|
}
|
|
1205
|
+
return data;
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
logger.warn(`Failed to parse cache from ${path}: ${err}`);
|
|
1208
|
+
return null;
|
|
846
1209
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1210
|
+
}
|
|
1211
|
+
function writeCache(baseUrl, entry) {
|
|
1212
|
+
const path = getCachePath(baseUrl);
|
|
1213
|
+
try {
|
|
1214
|
+
ensureCacheDir();
|
|
1215
|
+
const content = JSON.stringify(entry, null, 2);
|
|
1216
|
+
atomicWriteFile(path, content);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
logger.warn(`Failed to write cache to ${path}: ${error}`);
|
|
851
1219
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1220
|
+
}
|
|
1221
|
+
function isCacheValid(entry, currentEnv) {
|
|
1222
|
+
const fetchedAt = new Date(entry.fetchedAt).getTime();
|
|
1223
|
+
const now = Date.now();
|
|
1224
|
+
const age = now - fetchedAt;
|
|
1225
|
+
const ttlMs = entry.ttlSeconds * 1000;
|
|
1226
|
+
if (age >= ttlMs) {
|
|
1227
|
+
return false;
|
|
857
1228
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1229
|
+
if (entry.baseUrl !== currentEnv.baseUrl) {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
if (entry.version !== CACHE_VERSION) {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
if (entry.tokenHash !== currentEnv.tokenHash) {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
function isCacheProviderValid(entry, currentProvider) {
|
|
1241
|
+
return entry.provider === currentProvider;
|
|
1242
|
+
}
|
|
1243
|
+
function isCacheRenderedLineUsable(entry, currentConfigHash) {
|
|
1244
|
+
return entry.configHash === currentConfigHash;
|
|
1245
|
+
}
|
|
1246
|
+
var DEFAULT_POLL_INTERVAL_SECONDS = 30;
|
|
1247
|
+
function getEffectivePollInterval(config, envOverride) {
|
|
1248
|
+
if (envOverride !== null) {
|
|
1249
|
+
return Math.max(5, envOverride);
|
|
1250
|
+
}
|
|
1251
|
+
const fromConfig = config.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS;
|
|
1252
|
+
return Math.max(5, fromConfig);
|
|
1253
|
+
}
|
|
1254
|
+
function getProviderDetectionCachePath(baseUrl) {
|
|
1255
|
+
const hash = shortHash(baseUrl, 12);
|
|
1256
|
+
return join9(getCacheDir(), `provider-detect-${hash}.json`);
|
|
1257
|
+
}
|
|
1258
|
+
function readProviderDetectionCache(baseUrl) {
|
|
1259
|
+
const path = getProviderDetectionCachePath(baseUrl);
|
|
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;
|
|
861
1266
|
}
|
|
862
|
-
|
|
863
|
-
const entry = this.format(level, message, data);
|
|
864
|
-
appendFileSync(this.logPath, entry, { encoding: "utf-8" });
|
|
865
|
-
} catch {}
|
|
1267
|
+
throw err;
|
|
866
1268
|
}
|
|
867
|
-
|
|
868
|
-
|
|
1269
|
+
try {
|
|
1270
|
+
const data = JSON.parse(content);
|
|
1271
|
+
if (!isProviderDetectionCacheEntry(data)) {
|
|
1272
|
+
logger.warn(`Invalid provider detection cache structure at ${path}`);
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
const detectedAt = new Date(data.detectedAt).getTime();
|
|
1276
|
+
const now = Date.now();
|
|
1277
|
+
const age = now - detectedAt;
|
|
1278
|
+
const ttlMs = data.ttlSeconds * 1000;
|
|
1279
|
+
if (age >= ttlMs) {
|
|
1280
|
+
try {
|
|
1281
|
+
unlinkSync4(path);
|
|
1282
|
+
} catch {}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
return data;
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
logger.warn(`Failed to parse provider detection cache from ${path}: ${err}`);
|
|
1288
|
+
return null;
|
|
869
1289
|
}
|
|
870
|
-
|
|
871
|
-
|
|
1290
|
+
}
|
|
1291
|
+
function writeProviderDetectionCache(baseUrl, entry) {
|
|
1292
|
+
const path = getProviderDetectionCachePath(baseUrl);
|
|
1293
|
+
try {
|
|
1294
|
+
ensureCacheDir();
|
|
1295
|
+
const content = JSON.stringify(entry, null, 2);
|
|
1296
|
+
atomicWriteFile(path, content);
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
logger.warn(`Failed to write provider detection cache to ${path}: ${error}`);
|
|
872
1299
|
}
|
|
873
|
-
|
|
874
|
-
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/providers/autodetect.ts
|
|
1303
|
+
var detectionCache = new Map;
|
|
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";
|
|
1307
|
+
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1308
|
+
for (const [providerId, config] of Object.entries(endpointConfigs)) {
|
|
1309
|
+
const urlPatterns = config.detection?.urlPatterns;
|
|
1310
|
+
if (urlPatterns && urlPatterns.length > 0) {
|
|
1311
|
+
for (const pattern of urlPatterns) {
|
|
1312
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
1313
|
+
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1314
|
+
return providerId;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (includeBuiltInPatterns && (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats"))) {
|
|
1320
|
+
return "claude-relay-service";
|
|
1321
|
+
}
|
|
1322
|
+
return fallbackProvider;
|
|
1323
|
+
}
|
|
1324
|
+
async function resolveProvider(baseUrl, providerOverride, endpointConfigs = {}, probeTimeoutMs = 1500) {
|
|
1325
|
+
if (providerOverride) {
|
|
1326
|
+
logger.debug("Provider override detected", { provider: providerOverride });
|
|
1327
|
+
return providerOverride;
|
|
1328
|
+
}
|
|
1329
|
+
const cached = detectionCache.get(baseUrl);
|
|
1330
|
+
if (cached) {
|
|
1331
|
+
logger.debug("Provider detection cache hit (memory)", { provider: cached.provider });
|
|
1332
|
+
return cached.provider;
|
|
1333
|
+
}
|
|
1334
|
+
const diskCached = readProviderDetectionCache(baseUrl);
|
|
1335
|
+
if (diskCached) {
|
|
1336
|
+
logger.debug("Provider detection cache hit (disk)", {
|
|
1337
|
+
provider: diskCached.provider,
|
|
1338
|
+
detectedVia: diskCached.detectedVia
|
|
1339
|
+
});
|
|
1340
|
+
detectionCache.set(baseUrl, {
|
|
1341
|
+
provider: diskCached.provider,
|
|
1342
|
+
detectedAt: diskCached.detectedAt
|
|
1343
|
+
});
|
|
1344
|
+
return diskCached.provider;
|
|
1345
|
+
}
|
|
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;
|
|
1354
|
+
}
|
|
1355
|
+
logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
|
|
1356
|
+
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
|
|
1357
|
+
if (probedProvider) {
|
|
1358
|
+
logger.debug("Provider detected via health probe", { provider: probedProvider });
|
|
1359
|
+
cacheProviderDetection(baseUrl, probedProvider, "health-probe");
|
|
1360
|
+
return probedProvider;
|
|
1361
|
+
}
|
|
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
|
+
}
|
|
1368
|
+
logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
|
|
1369
|
+
cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
|
|
1370
|
+
return patternProvider;
|
|
1371
|
+
}
|
|
1372
|
+
function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
1373
|
+
const now = new Date().toISOString();
|
|
1374
|
+
detectionCache.set(baseUrl, {
|
|
1375
|
+
provider,
|
|
1376
|
+
detectedAt: now
|
|
1377
|
+
});
|
|
1378
|
+
writeProviderDetectionCache(baseUrl, {
|
|
1379
|
+
baseUrl,
|
|
1380
|
+
provider,
|
|
1381
|
+
detectedVia,
|
|
1382
|
+
detectedAt: now,
|
|
1383
|
+
ttlSeconds: PROVIDER_DETECTION_TTL_SECONDS
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
function clearDetectionCache() {
|
|
1387
|
+
detectionCache.clear();
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/cli/commands.ts
|
|
1391
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
|
|
1392
|
+
import { join as join10 } from "path";
|
|
1393
|
+
function handleInstall(args) {
|
|
1394
|
+
const existing = getExistingStatusLine();
|
|
1395
|
+
if (existing && !args.force) {
|
|
1396
|
+
console.error("Error: statusLine is already configured in settings.json");
|
|
1397
|
+
console.error(`Current command: ${existing}`);
|
|
1398
|
+
console.error("Use --force to overwrite, or --uninstall to remove first.");
|
|
1399
|
+
process.exit(1);
|
|
875
1400
|
}
|
|
876
|
-
|
|
877
|
-
|
|
1401
|
+
const runner = args.runner ?? (isBunxAvailable() ? "bunx" : "npx");
|
|
1402
|
+
console.log("Creating default configuration files...");
|
|
1403
|
+
writeDefaultConfigs();
|
|
1404
|
+
console.log("✓ Config files created:");
|
|
1405
|
+
console.log(" - ~/.claude/cc-api-statusline/config.json");
|
|
1406
|
+
console.log(" - ~/.claude/cc-api-statusline/api-config/sub2api.json");
|
|
1407
|
+
console.log(" - ~/.claude/cc-api-statusline/api-config/crs.json");
|
|
1408
|
+
console.log(" - ~/.claude/cc-api-statusline/.endpoint-config.lock");
|
|
1409
|
+
installStatusLine(runner);
|
|
1410
|
+
console.log("✓ Statusline installed successfully!");
|
|
1411
|
+
console.log(` Runner: ${runner}`);
|
|
1412
|
+
console.log(` Command: ${runner} -y cc-api-statusline@latest`);
|
|
1413
|
+
console.log(` Config: ~/.claude/settings.json`);
|
|
1414
|
+
process.exit(0);
|
|
1415
|
+
}
|
|
1416
|
+
function handleUninstall() {
|
|
1417
|
+
const existing = getExistingStatusLine();
|
|
1418
|
+
if (!existing) {
|
|
1419
|
+
console.log("No statusLine configuration found in settings.json");
|
|
1420
|
+
process.exit(0);
|
|
878
1421
|
}
|
|
879
|
-
|
|
880
|
-
|
|
1422
|
+
uninstallStatusLine();
|
|
1423
|
+
console.log("✓ Statusline uninstalled successfully");
|
|
1424
|
+
console.log(" Removed statusLine from ~/.claude/settings.json");
|
|
1425
|
+
process.exit(0);
|
|
1426
|
+
}
|
|
1427
|
+
function handleApplyConfig() {
|
|
1428
|
+
console.log("Applying endpoint configuration changes...");
|
|
1429
|
+
const currentHash = computeEndpointConfigHash();
|
|
1430
|
+
console.log(`Current endpoint config hash: ${currentHash}`);
|
|
1431
|
+
clearDetectionCache();
|
|
1432
|
+
console.log("✓ Provider detection cache cleared");
|
|
1433
|
+
const cacheDir = getCacheDir();
|
|
1434
|
+
let failCount = 0;
|
|
1435
|
+
if (existsSync5(cacheDir)) {
|
|
1436
|
+
const files = readdirSync3(cacheDir).filter((f) => f.startsWith("cache-") && f.endsWith(".json"));
|
|
1437
|
+
for (const file of files) {
|
|
1438
|
+
try {
|
|
1439
|
+
const filePath = join10(cacheDir, file);
|
|
1440
|
+
unlinkSync5(filePath);
|
|
1441
|
+
} catch {
|
|
1442
|
+
failCount++;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (failCount > 0) {
|
|
1446
|
+
console.log(`⚠ Cleared ${files.length - failCount}/${files.length} cache files (${failCount} failed)`);
|
|
1447
|
+
} else {
|
|
1448
|
+
console.log(`✓ Cleared ${files.length} data cache file(s)`);
|
|
1449
|
+
}
|
|
881
1450
|
}
|
|
882
|
-
|
|
883
|
-
|
|
1451
|
+
writeEndpointLock(currentHash);
|
|
1452
|
+
console.log("✓ Lock file updated");
|
|
1453
|
+
console.log("");
|
|
1454
|
+
console.log("✓ Endpoint config changes applied successfully!");
|
|
1455
|
+
console.log(" Changes will take effect on next statusline refresh.");
|
|
1456
|
+
console.log("");
|
|
1457
|
+
console.log("Config files:");
|
|
1458
|
+
const apiConfigDir = getEndpointConfigDir();
|
|
1459
|
+
if (existsSync5(apiConfigDir)) {
|
|
1460
|
+
const configFiles = readdirSync3(apiConfigDir).filter((f) => f.endsWith(".json"));
|
|
1461
|
+
for (const file of configFiles) {
|
|
1462
|
+
console.log(` - ${apiConfigDir}/${file}`);
|
|
1463
|
+
}
|
|
884
1464
|
}
|
|
1465
|
+
process.exit(0);
|
|
885
1466
|
}
|
|
886
|
-
var logger = new Logger;
|
|
887
|
-
|
|
888
1467
|
// src/services/user-agent.ts
|
|
1468
|
+
import { execSync as execSync2 } from "child_process";
|
|
1469
|
+
import { join as join11 } from "path";
|
|
1470
|
+
import { homedir as homedir3 } from "os";
|
|
889
1471
|
var FALLBACK_UA = "claude-cli/2.1.56 (external, cli)";
|
|
890
1472
|
function resolveUserAgent(config) {
|
|
891
1473
|
if (!config) {
|
|
@@ -912,10 +1494,10 @@ function detectClaudeVersion() {
|
|
|
912
1494
|
if (!process.env["CLAUDECODE"]) {
|
|
913
1495
|
return null;
|
|
914
1496
|
}
|
|
915
|
-
const claudePath =
|
|
1497
|
+
const claudePath = join11(homedir3(), ".claude", "bin", "claude");
|
|
916
1498
|
const result = execSync2(`"${claudePath}" --version`, {
|
|
917
1499
|
encoding: "utf-8",
|
|
918
|
-
timeout:
|
|
1500
|
+
timeout: 100,
|
|
919
1501
|
stdio: ["ignore", "pipe", "ignore"]
|
|
920
1502
|
});
|
|
921
1503
|
const match = result.match(/(\d+\.\d+\.\d+)/);
|
|
@@ -942,15 +1524,6 @@ function createQuotaWindow(used, limit, resetsAt) {
|
|
|
942
1524
|
};
|
|
943
1525
|
}
|
|
944
1526
|
|
|
945
|
-
// src/core/constants.ts
|
|
946
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 5000;
|
|
947
|
-
var EXIT_BUFFER_MS = 50;
|
|
948
|
-
var STALENESS_THRESHOLD_MINUTES = 5;
|
|
949
|
-
var VERY_STALE_THRESHOLD_MINUTES = 30;
|
|
950
|
-
var GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
951
|
-
var GC_MAX_CACHE_FILES = 20;
|
|
952
|
-
var GC_ORPHAN_TMP_AGE_MS = 60 * 60 * 1000;
|
|
953
|
-
|
|
954
1527
|
// src/providers/sub2api.ts
|
|
955
1528
|
function mapPeriodTokens(data) {
|
|
956
1529
|
if (!data)
|
|
@@ -1027,44 +1600,6 @@ async function fetchSub2api(baseUrl, token, config, timeoutMs = DEFAULT_FETCH_TI
|
|
|
1027
1600
|
};
|
|
1028
1601
|
}
|
|
1029
1602
|
|
|
1030
|
-
// src/providers/health-probe.ts
|
|
1031
|
-
function extractOrigin(baseUrl) {
|
|
1032
|
-
try {
|
|
1033
|
-
const url = new URL(baseUrl);
|
|
1034
|
-
return url.origin;
|
|
1035
|
-
} catch {
|
|
1036
|
-
return baseUrl;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
async function probeHealth(baseUrl, timeoutMs = 1500) {
|
|
1040
|
-
const origin = extractOrigin(baseUrl);
|
|
1041
|
-
const healthUrl = `${origin}/health`;
|
|
1042
|
-
logger.debug("Probing health endpoint", { healthUrl, timeoutMs });
|
|
1043
|
-
try {
|
|
1044
|
-
const responseText = await secureFetch(healthUrl, {
|
|
1045
|
-
method: "GET",
|
|
1046
|
-
headers: {
|
|
1047
|
-
Accept: "application/json"
|
|
1048
|
-
}
|
|
1049
|
-
}, timeoutMs);
|
|
1050
|
-
const data = JSON.parse(responseText);
|
|
1051
|
-
logger.debug("Health probe response", { data });
|
|
1052
|
-
if (typeof data["service"] === "string") {
|
|
1053
|
-
logger.debug("Detected provider from service field", { provider: data["service"] });
|
|
1054
|
-
return data["service"];
|
|
1055
|
-
}
|
|
1056
|
-
if (data["status"] === "ok") {
|
|
1057
|
-
logger.debug("Detected sub2api from status: ok pattern");
|
|
1058
|
-
return "sub2api";
|
|
1059
|
-
}
|
|
1060
|
-
logger.debug("Health probe returned unrecognized pattern", { data });
|
|
1061
|
-
return null;
|
|
1062
|
-
} catch (error) {
|
|
1063
|
-
logger.debug("Health probe failed", { error: String(error) });
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
1603
|
// src/services/time.ts
|
|
1069
1604
|
function computeNextMidnightLocal() {
|
|
1070
1605
|
const now = new Date;
|
|
@@ -1158,7 +1693,7 @@ async function fetchClaudeRelayService(baseUrl, token, config, timeoutMs = DEFAU
|
|
|
1158
1693
|
};
|
|
1159
1694
|
}
|
|
1160
1695
|
|
|
1161
|
-
// src/providers/
|
|
1696
|
+
// src/providers/response-mapping.ts
|
|
1162
1697
|
function resolveJsonPath(data, path) {
|
|
1163
1698
|
if (!path.startsWith("$.")) {
|
|
1164
1699
|
return path;
|
|
@@ -1236,11 +1771,11 @@ function extractTokenStatsPeriod(data, mapping, prefix) {
|
|
|
1236
1771
|
cost: extractNumber(data, mapping[`${prefix}.cost`]) ?? 0
|
|
1237
1772
|
};
|
|
1238
1773
|
}
|
|
1239
|
-
function mapResponseToUsage(responseData, mapping,
|
|
1774
|
+
function mapResponseToUsage(responseData, mapping, endpointConfig) {
|
|
1240
1775
|
const billingModeStr = extractString(responseData, mapping.billingMode, "subscription");
|
|
1241
1776
|
const billingMode = billingModeStr === "balance" ? "balance" : "subscription";
|
|
1242
|
-
const planName = extractString(responseData, mapping.planName,
|
|
1243
|
-
const base = createEmptyNormalizedUsage(
|
|
1777
|
+
const planName = extractString(responseData, mapping.planName, endpointConfig.displayName ?? endpointConfig.provider);
|
|
1778
|
+
const base = createEmptyNormalizedUsage(endpointConfig.provider, billingMode, planName);
|
|
1244
1779
|
const balance = mapping["balance.remaining"] ? (() => {
|
|
1245
1780
|
const remaining = extractNumber(responseData, mapping["balance.remaining"]);
|
|
1246
1781
|
if (remaining === null)
|
|
@@ -1277,19 +1812,17 @@ function mapResponseToUsage(responseData, mapping, providerConfig) {
|
|
|
1277
1812
|
remainingSeconds: extractNumber(responseData, mapping["rateLimit.remainingSeconds"]) ?? 0
|
|
1278
1813
|
};
|
|
1279
1814
|
})();
|
|
1280
|
-
const resetsAt = (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
return sorted[0] ?? null;
|
|
1292
|
-
})();
|
|
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
|
+
});
|
|
1293
1826
|
return {
|
|
1294
1827
|
...base,
|
|
1295
1828
|
resetSemantics: billingMode === "balance" ? "expiry" : "end-of-day",
|
|
@@ -1303,159 +1836,75 @@ function mapResponseToUsage(responseData, mapping, providerConfig) {
|
|
|
1303
1836
|
};
|
|
1304
1837
|
}
|
|
1305
1838
|
|
|
1306
|
-
// src/providers/
|
|
1307
|
-
function
|
|
1308
|
-
if (!
|
|
1309
|
-
return "
|
|
1310
|
-
if (!
|
|
1311
|
-
return "
|
|
1312
|
-
if (!
|
|
1313
|
-
return "
|
|
1314
|
-
if (!
|
|
1315
|
-
return "
|
|
1316
|
-
if (!
|
|
1317
|
-
return "
|
|
1318
|
-
if (!
|
|
1319
|
-
return "
|
|
1320
|
-
}
|
|
1321
|
-
if (!
|
|
1322
|
-
return "
|
|
1323
|
-
}
|
|
1324
|
-
if (
|
|
1325
|
-
return '
|
|
1326
|
-
}
|
|
1327
|
-
if (providerConfig.auth.type === "body" && !providerConfig.auth.bodyField) {
|
|
1328
|
-
return 'Custom provider auth.type="body" requires auth.bodyField';
|
|
1329
|
-
}
|
|
1330
|
-
if (providerConfig.urlPatterns && !Array.isArray(providerConfig.urlPatterns)) {
|
|
1331
|
-
return "Custom provider urlPatterns must be an array";
|
|
1839
|
+
// src/providers/endpoint-fetch.ts
|
|
1840
|
+
function validateEndpointConfigSemantics(config) {
|
|
1841
|
+
if (!config.provider)
|
|
1842
|
+
return "Endpoint config missing required field: provider";
|
|
1843
|
+
if (!config.endpoint?.path)
|
|
1844
|
+
return "Endpoint config missing required field: endpoint.path";
|
|
1845
|
+
if (!config.endpoint?.method)
|
|
1846
|
+
return "Endpoint config missing required field: endpoint.method";
|
|
1847
|
+
if (!config.auth)
|
|
1848
|
+
return "Endpoint config missing required field: auth";
|
|
1849
|
+
if (!config.responseMapping)
|
|
1850
|
+
return "Endpoint config missing required field: responseMapping";
|
|
1851
|
+
if (!config.endpoint.path.startsWith("/")) {
|
|
1852
|
+
return "Endpoint path must start with /";
|
|
1853
|
+
}
|
|
1854
|
+
if (config.auth.type === "custom-header" && !config.auth.header) {
|
|
1855
|
+
return 'Auth type="custom-header" requires auth.header';
|
|
1856
|
+
}
|
|
1857
|
+
if (config.auth.type === "body-key" && !config.auth.bodyField) {
|
|
1858
|
+
return 'Auth type="body-key" requires auth.bodyField';
|
|
1332
1859
|
}
|
|
1333
1860
|
return null;
|
|
1334
1861
|
}
|
|
1335
|
-
async function
|
|
1336
|
-
const validationError =
|
|
1862
|
+
async function fetchEndpoint(baseUrl, token, appConfig, endpointConfig, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
1863
|
+
const validationError = validateEndpointConfigSemantics(endpointConfig);
|
|
1337
1864
|
if (validationError) {
|
|
1338
|
-
throw new Error(`Invalid
|
|
1865
|
+
throw new Error(`Invalid endpoint config: ${validationError}`);
|
|
1339
1866
|
}
|
|
1340
|
-
const url = `${baseUrl}${
|
|
1867
|
+
const url = `${baseUrl}${endpointConfig.endpoint.path}`;
|
|
1341
1868
|
const headers = {
|
|
1342
1869
|
Accept: "application/json"
|
|
1343
1870
|
};
|
|
1344
|
-
if (
|
|
1345
|
-
headers["Content-Type"] =
|
|
1871
|
+
if (endpointConfig.endpoint.contentType) {
|
|
1872
|
+
headers["Content-Type"] = endpointConfig.endpoint.contentType;
|
|
1346
1873
|
}
|
|
1347
|
-
if (
|
|
1348
|
-
const prefix =
|
|
1349
|
-
headers[
|
|
1874
|
+
if (endpointConfig.auth.type === "bearer-header") {
|
|
1875
|
+
const prefix = endpointConfig.auth.prefix ?? "Bearer ";
|
|
1876
|
+
headers["Authorization"] = `${prefix}${token}`;
|
|
1877
|
+
} else if (endpointConfig.auth.type === "custom-header" && endpointConfig.auth.header) {
|
|
1878
|
+
const prefix = endpointConfig.auth.prefix ?? "";
|
|
1879
|
+
headers[endpointConfig.auth.header] = `${prefix}${token}`;
|
|
1350
1880
|
}
|
|
1351
1881
|
let body;
|
|
1352
|
-
if (
|
|
1353
|
-
if (
|
|
1354
|
-
const bodyObj = { ...
|
|
1355
|
-
bodyObj[
|
|
1882
|
+
if (endpointConfig.endpoint.method === "POST") {
|
|
1883
|
+
if (endpointConfig.auth.type === "body-key" && endpointConfig.auth.bodyField) {
|
|
1884
|
+
const bodyObj = { ...endpointConfig.requestBody ?? {} };
|
|
1885
|
+
bodyObj[endpointConfig.auth.bodyField] = token;
|
|
1356
1886
|
body = JSON.stringify(bodyObj);
|
|
1357
|
-
} else if (
|
|
1358
|
-
body = JSON.stringify(
|
|
1887
|
+
} else if (endpointConfig.requestBody) {
|
|
1888
|
+
body = JSON.stringify(endpointConfig.requestBody);
|
|
1359
1889
|
}
|
|
1360
1890
|
}
|
|
1361
|
-
const
|
|
1891
|
+
const endpointUA = endpointConfig.spoofClaudeCodeUA;
|
|
1362
1892
|
const globalUA = appConfig.spoofClaudeCodeUA;
|
|
1363
|
-
const effectiveUA =
|
|
1893
|
+
const effectiveUA = endpointUA !== undefined ? endpointUA : globalUA;
|
|
1364
1894
|
const resolvedUA = resolveUserAgent(effectiveUA);
|
|
1365
1895
|
if (resolvedUA) {
|
|
1366
|
-
logger.debug(`Using User-Agent for ${
|
|
1896
|
+
logger.debug(`Using User-Agent for ${endpointConfig.provider}: ${resolvedUA}`);
|
|
1367
1897
|
}
|
|
1368
1898
|
const responseText = await secureFetch(url, {
|
|
1369
|
-
method:
|
|
1899
|
+
method: endpointConfig.endpoint.method,
|
|
1370
1900
|
headers,
|
|
1371
1901
|
body
|
|
1372
1902
|
}, timeoutMs, resolvedUA);
|
|
1373
1903
|
const responseData = JSON.parse(responseText);
|
|
1374
|
-
const result = mapResponseToUsage(responseData,
|
|
1904
|
+
const result = mapResponseToUsage(responseData, endpointConfig.responseMapping, endpointConfig);
|
|
1375
1905
|
return result;
|
|
1376
1906
|
}
|
|
1377
1907
|
|
|
1378
|
-
// src/providers/autodetect.ts
|
|
1379
|
-
var detectionCache = new Map;
|
|
1380
|
-
function detectProviderFromUrlPattern(baseUrl, customProviders = {}) {
|
|
1381
|
-
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1382
|
-
for (const [providerId, config] of Object.entries(customProviders)) {
|
|
1383
|
-
if (config.urlPatterns && config.urlPatterns.length > 0) {
|
|
1384
|
-
for (const pattern of config.urlPatterns) {
|
|
1385
|
-
const normalizedPattern = pattern.toLowerCase();
|
|
1386
|
-
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1387
|
-
return providerId;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
if (normalizedUrl.includes("/apistats") || normalizedUrl.includes("/api/user-stats")) {
|
|
1393
|
-
return "claude-relay-service";
|
|
1394
|
-
}
|
|
1395
|
-
return "sub2api";
|
|
1396
|
-
}
|
|
1397
|
-
async function resolveProvider(baseUrl, providerOverride, customProviders = {}, probeTimeoutMs = 1500) {
|
|
1398
|
-
if (providerOverride) {
|
|
1399
|
-
logger.debug("Provider override detected", { provider: providerOverride });
|
|
1400
|
-
return providerOverride;
|
|
1401
|
-
}
|
|
1402
|
-
const cached = detectionCache.get(baseUrl);
|
|
1403
|
-
if (cached) {
|
|
1404
|
-
logger.debug("Provider detection cache hit (memory)", { provider: cached.provider });
|
|
1405
|
-
return cached.provider;
|
|
1406
|
-
}
|
|
1407
|
-
const diskCached = readProviderDetectionCache(baseUrl);
|
|
1408
|
-
if (diskCached) {
|
|
1409
|
-
logger.debug("Provider detection cache hit (disk)", {
|
|
1410
|
-
provider: diskCached.provider,
|
|
1411
|
-
detectedVia: diskCached.detectedVia
|
|
1412
|
-
});
|
|
1413
|
-
detectionCache.set(baseUrl, {
|
|
1414
|
-
provider: diskCached.provider,
|
|
1415
|
-
detectedAt: diskCached.detectedAt
|
|
1416
|
-
});
|
|
1417
|
-
return diskCached.provider;
|
|
1418
|
-
}
|
|
1419
|
-
for (const [providerId, config] of Object.entries(customProviders)) {
|
|
1420
|
-
if (config.urlPatterns && config.urlPatterns.length > 0) {
|
|
1421
|
-
const normalizedUrl = baseUrl.toLowerCase().replace(/\/$/, "");
|
|
1422
|
-
for (const pattern of config.urlPatterns) {
|
|
1423
|
-
const normalizedPattern = pattern.toLowerCase();
|
|
1424
|
-
if (normalizedUrl.includes(normalizedPattern)) {
|
|
1425
|
-
logger.debug("Provider detected via custom URL pattern", { provider: providerId, pattern });
|
|
1426
|
-
cacheProviderDetection(baseUrl, providerId, "url-pattern");
|
|
1427
|
-
return providerId;
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
logger.debug("Attempting health probe", { baseUrl, timeoutMs: probeTimeoutMs });
|
|
1433
|
-
const probedProvider = await probeHealth(baseUrl, probeTimeoutMs);
|
|
1434
|
-
if (probedProvider) {
|
|
1435
|
-
logger.debug("Provider detected via health probe", { provider: probedProvider });
|
|
1436
|
-
cacheProviderDetection(baseUrl, probedProvider, "health-probe");
|
|
1437
|
-
return probedProvider;
|
|
1438
|
-
}
|
|
1439
|
-
const patternProvider = detectProviderFromUrlPattern(baseUrl, {});
|
|
1440
|
-
logger.debug("Provider detected via built-in URL pattern", { provider: patternProvider });
|
|
1441
|
-
cacheProviderDetection(baseUrl, patternProvider, "url-pattern");
|
|
1442
|
-
return patternProvider;
|
|
1443
|
-
}
|
|
1444
|
-
function cacheProviderDetection(baseUrl, provider, detectedVia) {
|
|
1445
|
-
const now = new Date().toISOString();
|
|
1446
|
-
detectionCache.set(baseUrl, {
|
|
1447
|
-
provider,
|
|
1448
|
-
detectedAt: now
|
|
1449
|
-
});
|
|
1450
|
-
writeProviderDetectionCache(baseUrl, {
|
|
1451
|
-
baseUrl,
|
|
1452
|
-
provider,
|
|
1453
|
-
detectedVia,
|
|
1454
|
-
detectedAt: now,
|
|
1455
|
-
ttlSeconds: PROVIDER_DETECTION_TTL_SECONDS
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
1908
|
// src/providers/index.ts
|
|
1460
1909
|
var BUILT_IN_ADAPTERS = {
|
|
1461
1910
|
sub2api: {
|
|
@@ -1465,14 +1914,14 @@ var BUILT_IN_ADAPTERS = {
|
|
|
1465
1914
|
fetch: fetchClaudeRelayService
|
|
1466
1915
|
}
|
|
1467
1916
|
};
|
|
1468
|
-
function getProvider(providerId,
|
|
1917
|
+
function getProvider(providerId, endpointConfigs = {}) {
|
|
1469
1918
|
if (BUILT_IN_ADAPTERS[providerId]) {
|
|
1470
1919
|
return BUILT_IN_ADAPTERS[providerId];
|
|
1471
1920
|
}
|
|
1472
|
-
const
|
|
1473
|
-
if (
|
|
1921
|
+
const endpointConfig = endpointConfigs[providerId];
|
|
1922
|
+
if (endpointConfig) {
|
|
1474
1923
|
return {
|
|
1475
|
-
fetch: (baseUrl, token, config, timeoutMs) =>
|
|
1924
|
+
fetch: (baseUrl, token, config, timeoutMs) => fetchEndpoint(baseUrl, token, config, endpointConfig, timeoutMs)
|
|
1476
1925
|
};
|
|
1477
1926
|
}
|
|
1478
1927
|
return null;
|
|
@@ -1501,10 +1950,30 @@ var ANSI_COLORS = {
|
|
|
1501
1950
|
};
|
|
1502
1951
|
var THEME_COLORS = {
|
|
1503
1952
|
cool: "#56B6C2",
|
|
1504
|
-
comfortable: "#
|
|
1953
|
+
comfortable: "#5EBE8A",
|
|
1505
1954
|
warm: "#C9A84C",
|
|
1506
|
-
hot: "#
|
|
1507
|
-
critical: "#
|
|
1955
|
+
hot: "#D68B45",
|
|
1956
|
+
critical: "#D45A5A",
|
|
1957
|
+
"pastel-cool": "#BAD7F2",
|
|
1958
|
+
"pastel-comfortable": "#BAF2D8",
|
|
1959
|
+
"pastel-medium": "#BAF2BB",
|
|
1960
|
+
"pastel-warm": "#F2E2BA",
|
|
1961
|
+
"pastel-hot": "#F2BAC9",
|
|
1962
|
+
"bright-cool": "#90F1EF",
|
|
1963
|
+
"bright-comfortable": "#7BF1A8",
|
|
1964
|
+
"bright-medium": "#C1FBA4",
|
|
1965
|
+
"bright-warm": "#FFEF9F",
|
|
1966
|
+
"bright-hot": "#FFD6E0",
|
|
1967
|
+
"ocean-cool": "#0081A7",
|
|
1968
|
+
"ocean-comfortable": "#00AFB9",
|
|
1969
|
+
"ocean-medium": "#FDFCDC",
|
|
1970
|
+
"ocean-warm": "#FED9B7",
|
|
1971
|
+
"ocean-hot": "#F07167",
|
|
1972
|
+
"neutral-cool": "#D8E2DC",
|
|
1973
|
+
"neutral-comfortable": "#FFE5D9",
|
|
1974
|
+
"neutral-warm": "#FFCAD4",
|
|
1975
|
+
"neutral-hot": "#F4ACB7",
|
|
1976
|
+
"neutral-critical": "#9D8189"
|
|
1508
1977
|
};
|
|
1509
1978
|
var ANSI_RESET = "\x1B[0m";
|
|
1510
1979
|
var ANSI_DIM = "\x1B[2m";
|
|
@@ -1596,6 +2065,9 @@ function hexToRgb(hex) {
|
|
|
1596
2065
|
function dimText(text) {
|
|
1597
2066
|
return `${ANSI_DIM}${text}${ANSI_RESET}`;
|
|
1598
2067
|
}
|
|
2068
|
+
function stripAnsi(text) {
|
|
2069
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2070
|
+
}
|
|
1599
2071
|
function resolveColor(colorName, usagePercent, config) {
|
|
1600
2072
|
const effectiveColor = colorName ?? "auto";
|
|
1601
2073
|
if (effectiveColor.startsWith("#") || ANSI_COLORS[effectiveColor.toLowerCase()]) {
|
|
@@ -1614,9 +2086,6 @@ function resolveColor(colorName, usagePercent, config) {
|
|
|
1614
2086
|
}
|
|
1615
2087
|
return resolveColorAlias(alias, usagePercent);
|
|
1616
2088
|
}
|
|
1617
|
-
function isTieredEntry(alias) {
|
|
1618
|
-
return "tiers" in alias;
|
|
1619
|
-
}
|
|
1620
2089
|
function resolveTieredColor(entry, usagePercent) {
|
|
1621
2090
|
if (entry.tiers.length === 0)
|
|
1622
2091
|
return null;
|
|
@@ -1633,19 +2102,7 @@ function resolveTieredColor(entry, usagePercent) {
|
|
|
1633
2102
|
function resolveColorAlias(alias, usagePercent) {
|
|
1634
2103
|
if (!alias)
|
|
1635
2104
|
return null;
|
|
1636
|
-
|
|
1637
|
-
return resolveTieredColor(alias, usagePercent);
|
|
1638
|
-
}
|
|
1639
|
-
if (usagePercent === null) {
|
|
1640
|
-
return alias.low;
|
|
1641
|
-
}
|
|
1642
|
-
if (usagePercent < alias.lowThreshold) {
|
|
1643
|
-
return alias.low;
|
|
1644
|
-
} else if (usagePercent < alias.highThreshold) {
|
|
1645
|
-
return alias.medium;
|
|
1646
|
-
} else {
|
|
1647
|
-
return alias.high;
|
|
1648
|
-
}
|
|
2105
|
+
return resolveTieredColor(alias, usagePercent);
|
|
1649
2106
|
}
|
|
1650
2107
|
|
|
1651
2108
|
// src/renderer/transition.ts
|
|
@@ -1696,6 +2153,8 @@ function renderStandaloneError(errorState, provider, message) {
|
|
|
1696
2153
|
return `${warningIcon} Set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`;
|
|
1697
2154
|
case "timeout":
|
|
1698
2155
|
return `${warningIcon} Fetching...`;
|
|
2156
|
+
case "endpoint-config-changed":
|
|
2157
|
+
return `${warningIcon} Endpoint config changed — run: cc-api-statusline --apply-config`;
|
|
1699
2158
|
case "network-error":
|
|
1700
2159
|
case "server-error":
|
|
1701
2160
|
case "parse-error":
|
|
@@ -2003,46 +2462,49 @@ function formatCompactNumber(n) {
|
|
|
2003
2462
|
|
|
2004
2463
|
// src/renderer/component.ts
|
|
2005
2464
|
function renderComponent(componentId, data, componentConfig, globalConfig, renderContext) {
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
+
};
|
|
2012
2473
|
switch (componentId) {
|
|
2013
2474
|
case "daily":
|
|
2014
|
-
return renderQuotaComponent("daily", data.daily, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
|
|
2015
2475
|
case "weekly":
|
|
2016
|
-
return renderQuotaComponent("weekly", data.weekly, effectiveLayout, effectiveDisplayMode, effectiveProgressStyle, effectiveBarSize, effectiveBarStyle, componentConfig, globalConfig, clockFormat, renderContext);
|
|
2017
2476
|
case "monthly":
|
|
2018
|
-
return renderQuotaComponent(
|
|
2477
|
+
return renderQuotaComponent(componentId, data[componentId], options, componentConfig, globalConfig, renderContext);
|
|
2019
2478
|
case "balance":
|
|
2020
|
-
return renderBalanceComponent(data.balance,
|
|
2479
|
+
return renderBalanceComponent(data.balance, options, componentConfig, globalConfig, renderContext);
|
|
2021
2480
|
case "tokens":
|
|
2022
|
-
return renderTokensComponent(data.tokenStats,
|
|
2481
|
+
return renderTokensComponent(data.tokenStats, options, componentConfig, globalConfig, renderContext);
|
|
2023
2482
|
case "rateLimit":
|
|
2024
|
-
return renderRateLimitComponent(data.rateLimit,
|
|
2483
|
+
return renderRateLimitComponent(data.rateLimit, options, componentConfig, globalConfig, renderContext);
|
|
2025
2484
|
case "plan":
|
|
2026
|
-
return renderPlanComponent(data.planName,
|
|
2485
|
+
return renderPlanComponent(data.planName, options, componentConfig, globalConfig, renderContext);
|
|
2027
2486
|
default:
|
|
2028
2487
|
return null;
|
|
2029
2488
|
}
|
|
2030
2489
|
}
|
|
2031
|
-
function renderQuotaComponent(componentId, quota,
|
|
2490
|
+
function renderQuotaComponent(componentId, quota, options, componentConfig, globalConfig, renderContext) {
|
|
2491
|
+
const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
|
|
2032
2492
|
if (!quota)
|
|
2033
2493
|
return null;
|
|
2034
2494
|
const usagePercent = calculateUsagePercent(quota.used, quota.limit);
|
|
2035
2495
|
const label = renderLabel(componentId, displayMode, componentConfig, quota.qualifier);
|
|
2496
|
+
const showPercentage = componentConfig.percentage !== false;
|
|
2036
2497
|
const barColor = resolvePartColor("bar", usagePercent, componentConfig, globalConfig);
|
|
2037
|
-
const valueColor = resolvePartColor("value", usagePercent, componentConfig, globalConfig);
|
|
2498
|
+
const valueColor = showPercentage ? resolvePartColor("value", usagePercent, componentConfig, globalConfig) : null;
|
|
2038
2499
|
const labelColor = resolvePartColor("label", usagePercent, componentConfig, globalConfig);
|
|
2039
2500
|
const countdownColor = resolvePartColor("countdown", usagePercent, componentConfig, globalConfig);
|
|
2040
2501
|
const progress = renderProgress(progressStyle, usagePercent, barSize, barStyle, barColor, null, renderContext);
|
|
2041
|
-
const value = ansiColor(`${Math.round(usagePercent)}%`, valueColor, renderContext);
|
|
2502
|
+
const value = showPercentage ? ansiColor(`${Math.round(usagePercent)}%`, valueColor, renderContext) : "";
|
|
2042
2503
|
const countdown = renderSecondaryDisplay(quota.resetsAt, quota, componentConfig.countdown, countdownColor, clockFormat, renderContext);
|
|
2043
2504
|
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
2044
2505
|
}
|
|
2045
|
-
function renderBalanceComponent(balance,
|
|
2506
|
+
function renderBalanceComponent(balance, options, componentConfig, globalConfig, renderContext) {
|
|
2507
|
+
const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
|
|
2046
2508
|
if (!balance)
|
|
2047
2509
|
return null;
|
|
2048
2510
|
const isUnlimited = balance.remaining === -1;
|
|
@@ -2061,7 +2523,8 @@ function renderBalanceComponent(balance, layout, displayMode, progressStyle, bar
|
|
|
2061
2523
|
const countdown = "";
|
|
2062
2524
|
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
2063
2525
|
}
|
|
2064
|
-
function renderTokensComponent(tokenStats,
|
|
2526
|
+
function renderTokensComponent(tokenStats, options, componentConfig, globalConfig, renderContext) {
|
|
2527
|
+
const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
|
|
2065
2528
|
if (!tokenStats)
|
|
2066
2529
|
return null;
|
|
2067
2530
|
const stats = tokenStats.total ?? tokenStats.today;
|
|
@@ -2077,7 +2540,8 @@ function renderTokensComponent(tokenStats, layout, displayMode, componentConfig,
|
|
|
2077
2540
|
const countdown = "";
|
|
2078
2541
|
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
2079
2542
|
}
|
|
2080
|
-
function renderRateLimitComponent(rateLimit,
|
|
2543
|
+
function renderRateLimitComponent(rateLimit, options, componentConfig, globalConfig, renderContext) {
|
|
2544
|
+
const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
|
|
2081
2545
|
if (!rateLimit)
|
|
2082
2546
|
return null;
|
|
2083
2547
|
let usagePercent = null;
|
|
@@ -2094,7 +2558,8 @@ function renderRateLimitComponent(rateLimit, layout, displayMode, progressStyle,
|
|
|
2094
2558
|
const countdown = "";
|
|
2095
2559
|
return assembleComponent(layout, label, labelColor, progress, value, countdown, renderContext);
|
|
2096
2560
|
}
|
|
2097
|
-
function renderPlanComponent(planName,
|
|
2561
|
+
function renderPlanComponent(planName, options, componentConfig, globalConfig, renderContext) {
|
|
2562
|
+
const { layout, displayMode, progressStyle, barSize, barStyle, clockFormat } = options;
|
|
2098
2563
|
if (displayMode === "hidden")
|
|
2099
2564
|
return null;
|
|
2100
2565
|
const labelColor = resolvePartColor("label", null, componentConfig, globalConfig);
|
|
@@ -2208,21 +2673,25 @@ function assembleComponent(layout, label, labelColor, progress, value, countdown
|
|
|
2208
2673
|
if (layout === "percent-first") {
|
|
2209
2674
|
if (coloredLabel)
|
|
2210
2675
|
parts.push(coloredLabel);
|
|
2211
|
-
|
|
2676
|
+
if (value)
|
|
2677
|
+
parts.push(value);
|
|
2212
2678
|
if (progress)
|
|
2213
2679
|
parts.push(progress);
|
|
2214
|
-
if (countdown)
|
|
2215
|
-
parts.push(countdown);
|
|
2216
2680
|
} else {
|
|
2217
2681
|
if (coloredLabel)
|
|
2218
2682
|
parts.push(coloredLabel);
|
|
2219
2683
|
if (progress)
|
|
2220
2684
|
parts.push(progress);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2685
|
+
if (value)
|
|
2686
|
+
parts.push(value);
|
|
2687
|
+
}
|
|
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);
|
|
2224
2693
|
}
|
|
2225
|
-
return parts.
|
|
2694
|
+
return parts.join(" ");
|
|
2226
2695
|
}
|
|
2227
2696
|
function calculateUsagePercent(used, limit) {
|
|
2228
2697
|
if (limit === null)
|
|
@@ -2259,8 +2728,7 @@ function computeMaxWidth(termWidth, maxWidthPct) {
|
|
|
2259
2728
|
return Math.floor(termWidth * pct / 100);
|
|
2260
2729
|
}
|
|
2261
2730
|
function visibleLength(text) {
|
|
2262
|
-
|
|
2263
|
-
return stripped.length;
|
|
2731
|
+
return stripAnsi(text).length;
|
|
2264
2732
|
}
|
|
2265
2733
|
function ansiAwareTruncate(text, maxWidth) {
|
|
2266
2734
|
const visible = visibleLength(text);
|
|
@@ -2307,8 +2775,8 @@ var COMPONENT_DROP_PRIORITY = [
|
|
|
2307
2775
|
// src/renderer/divider.ts
|
|
2308
2776
|
function renderDivider(divider) {
|
|
2309
2777
|
const text = divider.text ?? "|";
|
|
2310
|
-
const
|
|
2311
|
-
const pad = " ".repeat(
|
|
2778
|
+
const margin = divider.margin ?? 1;
|
|
2779
|
+
const pad = " ".repeat(margin);
|
|
2312
2780
|
const padded = `${pad}${text}${pad}`;
|
|
2313
2781
|
return ansiColor(padded, divider.color ?? "#555753");
|
|
2314
2782
|
}
|
|
@@ -2400,63 +2868,46 @@ function renderStatusline(data, config, errorState, cacheAge, isPiped = false) {
|
|
|
2400
2868
|
currentWidth = calculateStatuslineWidth(componentMap, activeComponents, componentOrder, separator, errorState, data, cacheAge);
|
|
2401
2869
|
}
|
|
2402
2870
|
}
|
|
2403
|
-
|
|
2404
|
-
for (const componentId of componentOrder) {
|
|
2405
|
-
if (activeComponents.has(componentId)) {
|
|
2406
|
-
const rendered = componentMap.get(componentId);
|
|
2407
|
-
if (rendered)
|
|
2408
|
-
renderedComponents.push(rendered);
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
let statusline = renderedComponents.join(separator);
|
|
2412
|
-
if (errorState) {
|
|
2413
|
-
if (isTransitionState(errorState)) {
|
|
2414
|
-
statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
|
|
2415
|
-
} else {
|
|
2416
|
-
const hasCache = renderedComponents.length > 0;
|
|
2417
|
-
const errorMode = hasCache ? "with-cache" : "without-cache";
|
|
2418
|
-
const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
|
|
2419
|
-
statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2871
|
+
let statusline = assembleStatuslineString(componentOrder, componentMap, activeComponents, separator, errorState, data, cacheAge);
|
|
2422
2872
|
const termWidth = getTerminalWidth();
|
|
2423
2873
|
const maxW = computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
|
|
2424
2874
|
statusline = ansiAwareTruncate(statusline, maxW);
|
|
2425
2875
|
return statusline;
|
|
2426
2876
|
}
|
|
2427
2877
|
function computeSeparator(config) {
|
|
2428
|
-
const dividerConfig = config.
|
|
2878
|
+
const dividerConfig = config.display.divider;
|
|
2429
2879
|
if (dividerConfig === false)
|
|
2430
2880
|
return "";
|
|
2431
|
-
|
|
2432
|
-
return renderDivider(dividerConfig);
|
|
2433
|
-
return config.display.separator ?? " | ";
|
|
2881
|
+
return renderDivider(dividerConfig ?? DEFAULT_DIVIDER_CONFIG);
|
|
2434
2882
|
}
|
|
2435
2883
|
function maxWidth(config) {
|
|
2436
2884
|
const termWidth = getTerminalWidth();
|
|
2437
2885
|
return computeMaxWidth(termWidth, config.display.maxWidth ?? 100);
|
|
2438
2886
|
}
|
|
2439
|
-
function
|
|
2440
|
-
const
|
|
2887
|
+
function assembleStatuslineString(componentOrder, componentMap, activeComponents, separator, errorState, data, cacheAge) {
|
|
2888
|
+
const rendered = [];
|
|
2441
2889
|
for (const id of componentOrder) {
|
|
2442
2890
|
if (activeComponents.has(id)) {
|
|
2443
|
-
const
|
|
2444
|
-
if (
|
|
2445
|
-
|
|
2891
|
+
const r = componentMap.get(id);
|
|
2892
|
+
if (r)
|
|
2893
|
+
rendered.push(r);
|
|
2446
2894
|
}
|
|
2447
2895
|
}
|
|
2448
|
-
let statusline =
|
|
2896
|
+
let statusline = rendered.join(separator);
|
|
2449
2897
|
if (errorState) {
|
|
2450
2898
|
if (isTransitionState(errorState)) {
|
|
2451
2899
|
statusline = renderError(errorState, "with-cache", data.provider, undefined, cacheAge);
|
|
2452
2900
|
} else {
|
|
2453
|
-
const hasCache =
|
|
2901
|
+
const hasCache = rendered.length > 0;
|
|
2454
2902
|
const errorMode = hasCache ? "with-cache" : "without-cache";
|
|
2455
2903
|
const errorIndicator = renderError(errorState, errorMode, data.provider, undefined, cacheAge);
|
|
2456
2904
|
statusline = hasCache ? `${statusline} ${errorIndicator}` : errorIndicator;
|
|
2457
2905
|
}
|
|
2458
2906
|
}
|
|
2459
|
-
return
|
|
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));
|
|
2460
2911
|
}
|
|
2461
2912
|
function getComponentOrder(config) {
|
|
2462
2913
|
const explicitOrder = [];
|
|
@@ -2481,10 +2932,10 @@ function isComponentId(key) {
|
|
|
2481
2932
|
|
|
2482
2933
|
// src/core/execute-cycle.ts
|
|
2483
2934
|
async function executeCycle(ctx) {
|
|
2484
|
-
const { env, config, configHash, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
2935
|
+
const { env, config, configHash, endpointConfigHash, endpointLock, cachedEntry, providerId, provider, timeoutBudgetMs, startTime, fetchTimeoutMs } = ctx;
|
|
2485
2936
|
if (cachedEntry) {
|
|
2486
2937
|
if (isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
2487
|
-
if (cachedEntry.renderedLine && isCacheRenderedLineUsable(cachedEntry, configHash)) {
|
|
2938
|
+
if (cachedEntry.renderedLine && isCacheRenderedLineUsable(cachedEntry, configHash) && cachedEntry.endpointConfigHash === endpointConfigHash) {
|
|
2488
2939
|
logger.debug("Path A: Fast path (cached renderedLine)", {
|
|
2489
2940
|
cacheAge: `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s`
|
|
2490
2941
|
});
|
|
@@ -2496,13 +2947,34 @@ async function executeCycle(ctx) {
|
|
|
2496
2947
|
}
|
|
2497
2948
|
}
|
|
2498
2949
|
}
|
|
2950
|
+
if (endpointLock && endpointLock.hash !== endpointConfigHash) {
|
|
2951
|
+
logger.debug("Path B2: Endpoint config changed (locked out)", {
|
|
2952
|
+
lockedHash: endpointLock.hash,
|
|
2953
|
+
currentHash: endpointConfigHash
|
|
2954
|
+
});
|
|
2955
|
+
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
2956
|
+
const statusline = renderStatusline(cachedEntry.data, config);
|
|
2957
|
+
return {
|
|
2958
|
+
output: statusline,
|
|
2959
|
+
exitCode: 0,
|
|
2960
|
+
cacheUpdate: null
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
const errorOutput = renderError("endpoint-config-changed", "without-cache");
|
|
2964
|
+
return {
|
|
2965
|
+
output: errorOutput,
|
|
2966
|
+
exitCode: 0,
|
|
2967
|
+
cacheUpdate: null
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2499
2970
|
if (cachedEntry && isCacheValid(cachedEntry, env) && isCacheProviderValid(cachedEntry, providerId)) {
|
|
2500
2971
|
logger.debug("Path B: Re-render (config changed, cache data valid)");
|
|
2501
2972
|
const statusline = renderStatusline(cachedEntry.data, config);
|
|
2502
2973
|
const updatedEntry = {
|
|
2503
2974
|
...cachedEntry,
|
|
2504
2975
|
renderedLine: statusline,
|
|
2505
|
-
configHash
|
|
2976
|
+
configHash,
|
|
2977
|
+
endpointConfigHash
|
|
2506
2978
|
};
|
|
2507
2979
|
return {
|
|
2508
2980
|
output: statusline,
|
|
@@ -2548,6 +3020,7 @@ async function executeCycle(ctx) {
|
|
|
2548
3020
|
data,
|
|
2549
3021
|
renderedLine: statusline,
|
|
2550
3022
|
configHash,
|
|
3023
|
+
endpointConfigHash,
|
|
2551
3024
|
errorState: null
|
|
2552
3025
|
};
|
|
2553
3026
|
return {
|
|
@@ -2582,23 +3055,23 @@ async function executeCycle(ctx) {
|
|
|
2582
3055
|
}
|
|
2583
3056
|
}
|
|
2584
3057
|
// src/services/cache-gc.ts
|
|
2585
|
-
import { readdirSync, statSync, unlinkSync as
|
|
2586
|
-
import { join as
|
|
3058
|
+
import { readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync6, existsSync as existsSync6 } from "fs";
|
|
3059
|
+
import { join as join12 } from "path";
|
|
2587
3060
|
function runCacheGC(cacheDir) {
|
|
2588
3061
|
try {
|
|
2589
|
-
if (!
|
|
3062
|
+
if (!existsSync6(cacheDir)) {
|
|
2590
3063
|
logger.debug("GC: Cache directory does not exist, skipping", { cacheDir });
|
|
2591
3064
|
return;
|
|
2592
3065
|
}
|
|
2593
3066
|
logger.debug("GC: Starting garbage collection", { cacheDir });
|
|
2594
|
-
const files =
|
|
3067
|
+
const files = readdirSync4(cacheDir);
|
|
2595
3068
|
const cacheFiles = [];
|
|
2596
3069
|
const providerDetectFiles = [];
|
|
2597
3070
|
const tmpFiles = [];
|
|
2598
3071
|
for (const file of files) {
|
|
2599
3072
|
try {
|
|
2600
|
-
const filePath =
|
|
2601
|
-
const stats =
|
|
3073
|
+
const filePath = join12(cacheDir, file);
|
|
3074
|
+
const stats = statSync2(filePath);
|
|
2602
3075
|
const mtime = stats.mtimeMs;
|
|
2603
3076
|
if (file.startsWith("cache-") && file.endsWith(".json")) {
|
|
2604
3077
|
cacheFiles.push({ name: file, mtime });
|
|
@@ -2617,7 +3090,7 @@ function runCacheGC(cacheDir) {
|
|
|
2617
3090
|
const age = now - file.mtime;
|
|
2618
3091
|
if (age > GC_MAX_AGE_MS) {
|
|
2619
3092
|
try {
|
|
2620
|
-
|
|
3093
|
+
unlinkSync6(join12(cacheDir, file.name));
|
|
2621
3094
|
deletedCount++;
|
|
2622
3095
|
logger.debug("GC: Deleted old cache file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
|
|
2623
3096
|
} catch (error) {
|
|
@@ -2629,7 +3102,7 @@ function runCacheGC(cacheDir) {
|
|
|
2629
3102
|
const age = now - file.mtime;
|
|
2630
3103
|
if (age > GC_MAX_AGE_MS) {
|
|
2631
3104
|
try {
|
|
2632
|
-
|
|
3105
|
+
unlinkSync6(join12(cacheDir, file.name));
|
|
2633
3106
|
deletedCount++;
|
|
2634
3107
|
logger.debug("GC: Deleted old provider-detect file", { file: file.name, ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
|
|
2635
3108
|
} catch (error) {
|
|
@@ -2641,7 +3114,7 @@ function runCacheGC(cacheDir) {
|
|
|
2641
3114
|
const age = now - file.mtime;
|
|
2642
3115
|
if (age > GC_ORPHAN_TMP_AGE_MS) {
|
|
2643
3116
|
try {
|
|
2644
|
-
|
|
3117
|
+
unlinkSync6(join12(cacheDir, file.name));
|
|
2645
3118
|
deletedCount++;
|
|
2646
3119
|
logger.debug("GC: Deleted orphaned tmp file", { file: file.name, ageMinutes: Math.floor(age / (60 * 1000)) });
|
|
2647
3120
|
} catch (error) {
|
|
@@ -2658,7 +3131,7 @@ function runCacheGC(cacheDir) {
|
|
|
2658
3131
|
const toDelete = remainingCacheFiles.slice(0, remainingCacheFiles.length - GC_MAX_CACHE_FILES);
|
|
2659
3132
|
for (const file of toDelete) {
|
|
2660
3133
|
try {
|
|
2661
|
-
|
|
3134
|
+
unlinkSync6(join12(cacheDir, file.name));
|
|
2662
3135
|
deletedCount++;
|
|
2663
3136
|
logger.debug("GC: Deleted cache file (count limit)", { file: file.name });
|
|
2664
3137
|
} catch (error) {
|
|
@@ -2673,60 +3146,121 @@ function runCacheGC(cacheDir) {
|
|
|
2673
3146
|
}
|
|
2674
3147
|
|
|
2675
3148
|
// src/cli/piped-mode.ts
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3149
|
+
class StatuslineError extends Error {
|
|
3150
|
+
errorType;
|
|
3151
|
+
constructor(errorType) {
|
|
3152
|
+
super(errorType);
|
|
3153
|
+
this.errorType = errorType;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
function safeStdoutWrite(data) {
|
|
3157
|
+
try {
|
|
3158
|
+
process.stdout["write"](data);
|
|
3159
|
+
} catch {}
|
|
3160
|
+
}
|
|
3161
|
+
function readAndValidateEnv() {
|
|
3162
|
+
const env = readCurrentEnv();
|
|
3163
|
+
logger.debug("Environment loaded", {
|
|
3164
|
+
baseUrl: env.baseUrl ? `${env.baseUrl.substring(0, 30)}...` : undefined,
|
|
3165
|
+
hasToken: !!env.authToken,
|
|
3166
|
+
providerOverride: env.providerOverride,
|
|
3167
|
+
pollIntervalOverride: env.pollIntervalOverride
|
|
3168
|
+
});
|
|
3169
|
+
const envError = validateRequiredEnv(env);
|
|
3170
|
+
if (envError) {
|
|
3171
|
+
throw new StatuslineError("missing-env");
|
|
3172
|
+
}
|
|
3173
|
+
const { baseUrl } = env;
|
|
3174
|
+
if (!baseUrl) {
|
|
3175
|
+
process.exit(0);
|
|
3176
|
+
}
|
|
3177
|
+
return { env, baseUrl };
|
|
3178
|
+
}
|
|
3179
|
+
function ensureDefaultConfigs() {
|
|
3180
|
+
if (needsConfigInit()) {
|
|
3181
|
+
logger.debug("First run detected - initializing default configs");
|
|
3182
|
+
writeDefaultConfigs();
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
function loadEndpointConfigsWithHash() {
|
|
3186
|
+
const endpointConfigs = loadEndpointConfigs();
|
|
3187
|
+
const endpointConfigHash = computeEndpointConfigHash();
|
|
3188
|
+
logger.debug("Endpoint configs loaded", {
|
|
3189
|
+
configCount: Object.keys(endpointConfigs).length,
|
|
3190
|
+
endpointConfigHash
|
|
3191
|
+
});
|
|
3192
|
+
return { endpointConfigs, endpointConfigHash };
|
|
3193
|
+
}
|
|
3194
|
+
function resolveEndpointLock(hash) {
|
|
3195
|
+
const existing = readEndpointLock();
|
|
3196
|
+
if (existing) {
|
|
3197
|
+
logger.debug("Endpoint lock file loaded", {
|
|
3198
|
+
lockedHash: existing.hash,
|
|
3199
|
+
currentHash: hash,
|
|
3200
|
+
locked: existing.hash === hash
|
|
2684
3201
|
});
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3202
|
+
return existing;
|
|
3203
|
+
}
|
|
3204
|
+
logger.debug("Endpoint lock file missing - creating with current hash");
|
|
3205
|
+
writeEndpointLock(hash);
|
|
3206
|
+
return { hash, lockedAt: new Date().toISOString() };
|
|
3207
|
+
}
|
|
3208
|
+
async function resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, timeoutMs) {
|
|
3209
|
+
const probeTimeout = isPiped ? Math.min(1500, Math.max(200, timeoutMs - 200)) : 3000;
|
|
3210
|
+
const providerId = await resolveProvider(baseUrl, env.providerOverride, endpointConfigs, probeTimeout);
|
|
3211
|
+
const provider = getProvider(providerId, endpointConfigs);
|
|
3212
|
+
logger.debug("Provider resolved", { providerId, probeTimeout });
|
|
3213
|
+
if (!provider) {
|
|
3214
|
+
logger.error("Provider not found", { providerId });
|
|
3215
|
+
throw new StatuslineError("provider-unknown");
|
|
3216
|
+
}
|
|
3217
|
+
return { providerId, provider };
|
|
3218
|
+
}
|
|
3219
|
+
function computeTimeoutBudgets(isPiped, config, timeoutMs) {
|
|
3220
|
+
const timeoutBudgetMs = isPiped ? timeoutMs : 1e4;
|
|
3221
|
+
const fetchTimeoutMs = isPiped ? Math.min(config.pipedRequestTimeoutMs ?? 800, timeoutBudgetMs - 100) : 1e4;
|
|
3222
|
+
return { timeoutBudgetMs, fetchTimeoutMs };
|
|
3223
|
+
}
|
|
3224
|
+
async function buildExecutionContext(args, isPiped, startTime, rawTimeoutMs) {
|
|
3225
|
+
const { env, baseUrl } = readAndValidateEnv();
|
|
3226
|
+
ensureDefaultConfigs();
|
|
3227
|
+
const { config, configHash } = loadConfigWithHash(args.configPath);
|
|
3228
|
+
const { endpointConfigs, endpointConfigHash } = loadEndpointConfigsWithHash();
|
|
3229
|
+
const endpointLock = resolveEndpointLock(endpointConfigHash);
|
|
3230
|
+
const cachedEntry = readCache(baseUrl);
|
|
3231
|
+
logger.debug("Cache read", {
|
|
3232
|
+
cacheHit: !!cachedEntry,
|
|
3233
|
+
cacheAge: cachedEntry ? `${Math.floor((Date.now() - new Date(cachedEntry.fetchedAt).getTime()) / 1000)}s` : "N/A"
|
|
3234
|
+
});
|
|
3235
|
+
let providerId;
|
|
3236
|
+
let provider;
|
|
3237
|
+
if (cachedEntry && isCacheValid(cachedEntry, env)) {
|
|
3238
|
+
const cachedProvider = getProvider(cachedEntry.provider, endpointConfigs);
|
|
3239
|
+
if (cachedProvider) {
|
|
3240
|
+
providerId = cachedEntry.provider;
|
|
3241
|
+
provider = cachedProvider;
|
|
3242
|
+
logger.debug("Cache-first: skipping provider probe", { providerId });
|
|
3243
|
+
} else {
|
|
3244
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
2709
3245
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
return { ctx, baseUrl };
|
|
2729
|
-
})();
|
|
3246
|
+
} else {
|
|
3247
|
+
({ providerId, provider } = await resolveProviderWithTimeout(baseUrl, env, endpointConfigs, isPiped, rawTimeoutMs));
|
|
3248
|
+
}
|
|
3249
|
+
const { timeoutBudgetMs, fetchTimeoutMs } = computeTimeoutBudgets(isPiped, config, rawTimeoutMs);
|
|
3250
|
+
const ctx = {
|
|
3251
|
+
env,
|
|
3252
|
+
config,
|
|
3253
|
+
configHash,
|
|
3254
|
+
endpointConfigHash,
|
|
3255
|
+
endpointLock,
|
|
3256
|
+
cachedEntry,
|
|
3257
|
+
providerId,
|
|
3258
|
+
provider,
|
|
3259
|
+
timeoutBudgetMs,
|
|
3260
|
+
startTime,
|
|
3261
|
+
fetchTimeoutMs
|
|
3262
|
+
};
|
|
3263
|
+
return { ctx, baseUrl };
|
|
2730
3264
|
}
|
|
2731
3265
|
function formatOutput(output, isPiped) {
|
|
2732
3266
|
let normalizedOutput = output;
|
|
@@ -2748,12 +3282,45 @@ async function executePipedMode(args) {
|
|
|
2748
3282
|
logger.debug("Start time", { startTime });
|
|
2749
3283
|
const isPiped = !process.stdin.isTTY;
|
|
2750
3284
|
logger.debug("Mode detection", { isPiped, once: args.once });
|
|
2751
|
-
const
|
|
3285
|
+
const rawTimeoutMs = Number(process.env["CC_STATUSLINE_TIMEOUT"] ?? 1000);
|
|
3286
|
+
if (isPiped) {
|
|
3287
|
+
const watchdogMs = rawTimeoutMs - 100;
|
|
3288
|
+
setTimeout(() => {
|
|
3289
|
+
logger.error("Watchdog timeout - forcing clean exit", { watchdogMs });
|
|
3290
|
+
const fallback = dimText("⟳ Refreshing...");
|
|
3291
|
+
const formatted = formatOutput(fallback, isPiped);
|
|
3292
|
+
safeStdoutWrite(formatted);
|
|
3293
|
+
process.exit(0);
|
|
3294
|
+
}, watchdogMs).unref();
|
|
3295
|
+
}
|
|
3296
|
+
let ctx;
|
|
3297
|
+
let baseUrl;
|
|
3298
|
+
try {
|
|
3299
|
+
({ ctx, baseUrl } = await buildExecutionContext(args, isPiped, startTime, rawTimeoutMs));
|
|
3300
|
+
} catch (error) {
|
|
3301
|
+
logger.error("Failed to build execution context", { error: String(error) });
|
|
3302
|
+
const errorType = error instanceof StatuslineError ? error.errorType : "network-error";
|
|
3303
|
+
const errorOutput = renderError(errorType, "without-cache");
|
|
3304
|
+
const formattedOutput2 = formatOutput(errorOutput, isPiped);
|
|
3305
|
+
safeStdoutWrite(formattedOutput2);
|
|
3306
|
+
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3307
|
+
process.exit(0);
|
|
3308
|
+
}
|
|
2752
3309
|
logger.debug("Execution context prepared", {
|
|
2753
3310
|
timeoutBudgetMs: ctx.timeoutBudgetMs,
|
|
2754
3311
|
fetchTimeoutMs: ctx.fetchTimeoutMs
|
|
2755
3312
|
});
|
|
2756
|
-
|
|
3313
|
+
let result;
|
|
3314
|
+
try {
|
|
3315
|
+
result = await executeCycle(ctx);
|
|
3316
|
+
} catch (error) {
|
|
3317
|
+
logger.error("Execution cycle failed", { error: String(error) });
|
|
3318
|
+
const errorOutput = renderError("network-error", "without-cache");
|
|
3319
|
+
const formattedOutput2 = formatOutput(errorOutput, isPiped);
|
|
3320
|
+
safeStdoutWrite(formattedOutput2);
|
|
3321
|
+
logger.debug("=== cc-api-statusline execution completed ===");
|
|
3322
|
+
process.exit(0);
|
|
3323
|
+
}
|
|
2757
3324
|
const executionTime = Date.now() - startTime;
|
|
2758
3325
|
logger.debug("Execution completed", {
|
|
2759
3326
|
exitCode: result.exitCode,
|
|
@@ -2762,7 +3329,7 @@ async function executePipedMode(args) {
|
|
|
2762
3329
|
cacheUpdate: !!result.cacheUpdate
|
|
2763
3330
|
});
|
|
2764
3331
|
const formattedOutput = formatOutput(result.output, isPiped);
|
|
2765
|
-
|
|
3332
|
+
safeStdoutWrite(formattedOutput);
|
|
2766
3333
|
if (result.cacheUpdate) {
|
|
2767
3334
|
writeCache(baseUrl, result.cacheUpdate);
|
|
2768
3335
|
logger.debug("Cache written", { baseUrl });
|
|
@@ -2776,6 +3343,7 @@ function discardStdin() {
|
|
|
2776
3343
|
if (!process.stdin.isTTY) {
|
|
2777
3344
|
process.stdin.resume();
|
|
2778
3345
|
process.stdin.on("data", () => {});
|
|
3346
|
+
process.stdin.on("error", () => {});
|
|
2779
3347
|
}
|
|
2780
3348
|
}
|
|
2781
3349
|
async function main() {
|
|
@@ -2799,6 +3367,10 @@ async function main() {
|
|
|
2799
3367
|
handleUninstall();
|
|
2800
3368
|
return;
|
|
2801
3369
|
}
|
|
3370
|
+
if (args.applyConfig) {
|
|
3371
|
+
handleApplyConfig();
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
2802
3374
|
if (process.stdin.isTTY && !args.once) {
|
|
2803
3375
|
console.log("Interactive configuration mode coming soon.");
|
|
2804
3376
|
console.log("Use --once for a single fetch, or configure as a Claude Code statusline command.");
|
|
@@ -2806,8 +3378,14 @@ async function main() {
|
|
|
2806
3378
|
}
|
|
2807
3379
|
await executePipedMode(args);
|
|
2808
3380
|
}
|
|
3381
|
+
process.on("SIGTERM", () => {
|
|
3382
|
+
process.exit(0);
|
|
3383
|
+
});
|
|
3384
|
+
process.on("uncaughtException", (error) => {
|
|
3385
|
+
logger.error("Uncaught exception", { error: String(error) });
|
|
3386
|
+
process.exit(0);
|
|
3387
|
+
});
|
|
2809
3388
|
main().catch((error) => {
|
|
2810
3389
|
logger.error("Unhandled error in main", { error: String(error) });
|
|
2811
|
-
|
|
2812
|
-
process.exit(1);
|
|
3390
|
+
process.exit(0);
|
|
2813
3391
|
});
|