@zhangferry-dev/tokendash 1.6.1 → 1.7.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.
Files changed (61) hide show
  1. package/README.md +148 -84
  2. package/dist/client/assets/index-Bw503sNp.css +1 -0
  3. package/dist/client/index.html +2 -2
  4. package/dist/daemon.cjs +3411 -0
  5. package/dist/daemon.cjs.map +7 -0
  6. package/dist/electron-server.cjs +1124 -28
  7. package/dist/electron-server.cjs.map +4 -4
  8. package/dist/server/ccusage.d.ts +7 -0
  9. package/dist/server/ccusage.js +69 -0
  10. package/dist/server/daemon.d.ts +12 -0
  11. package/dist/server/daemon.js +176 -0
  12. package/dist/server/index.js +23 -11
  13. package/dist/server/insightsCalculator.d.ts +15 -0
  14. package/dist/server/insightsCalculator.js +276 -0
  15. package/dist/server/quota/adapter.d.ts +49 -0
  16. package/dist/server/quota/adapter.js +41 -0
  17. package/dist/server/quota/adapters/claude.d.ts +4 -0
  18. package/dist/server/quota/adapters/claude.js +152 -0
  19. package/dist/server/quota/adapters/codex.d.ts +16 -0
  20. package/dist/server/quota/adapters/codex.js +226 -0
  21. package/dist/server/quota/adapters/glm.d.ts +2 -0
  22. package/dist/server/quota/adapters/glm.js +139 -0
  23. package/dist/server/quota/adapters/kimi.d.ts +2 -0
  24. package/dist/server/quota/adapters/kimi.js +186 -0
  25. package/dist/server/quota/adapters/minimax.d.ts +2 -0
  26. package/dist/server/quota/adapters/minimax.js +82 -0
  27. package/dist/server/quota/cache.d.ts +20 -0
  28. package/dist/server/quota/cache.js +44 -0
  29. package/dist/server/quota/credentialsFile.d.ts +13 -0
  30. package/dist/server/quota/credentialsFile.js +23 -0
  31. package/dist/server/quota/helpers.d.ts +39 -0
  32. package/dist/server/quota/helpers.js +93 -0
  33. package/dist/server/quota/index.d.ts +5 -0
  34. package/dist/server/quota/index.js +23 -0
  35. package/dist/server/quota/quotaService.d.ts +43 -0
  36. package/dist/server/quota/quotaService.js +163 -0
  37. package/dist/server/quota/schemas.d.ts +358 -0
  38. package/dist/server/quota/schemas.js +53 -0
  39. package/dist/server/quota/types.d.ts +76 -0
  40. package/dist/server/quota/types.js +10 -0
  41. package/dist/server/routes/api.js +34 -0
  42. package/dist/server/routes/insights.d.ts +2 -0
  43. package/dist/server/routes/insights.js +155 -0
  44. package/package.json +9 -11
  45. package/resources/entitlements.mac.plist +10 -0
  46. package/resources/icon-1024.png +0 -0
  47. package/resources/icon.icns +0 -0
  48. package/resources/icon.png +0 -0
  49. package/resources/product_menu.png +0 -0
  50. package/resources/readme-hero.png +0 -0
  51. package/dist/client/assets/index-_yA9tOzZ.css +0 -1
  52. package/electron/main.cjs +0 -516
  53. package/electron/npmSync.cjs +0 -62
  54. package/electron/preload.cjs +0 -36
  55. package/electron/serverReuse.cjs +0 -59
  56. package/electron/trayBadge.cjs +0 -27
  57. package/electron/trayHelper +0 -0
  58. package/electron/trayHelper.swift +0 -152
  59. package/electron/updateService.cjs +0 -220
  60. package/electron-builder.yml +0 -20
  61. /package/dist/client/assets/{index-CY4G_b0x.js → index-C913wKtU.js} +0 -0
@@ -37,9 +37,9 @@ __export(index_exports, {
37
37
  });
38
38
  module.exports = __toCommonJS(index_exports);
39
39
  var import_express = __toESM(require("express"), 1);
40
- var import_node_fs8 = require("node:fs");
40
+ var import_node_fs13 = require("node:fs");
41
41
  var import_node_url = require("node:url");
42
- var import_node_path8 = require("node:path");
42
+ var import_node_path13 = require("node:path");
43
43
 
44
44
  // src/server/cache.ts
45
45
  var import_node_fs = require("node:fs");
@@ -2088,7 +2088,1090 @@ function detectAvailableAgents() {
2088
2088
  };
2089
2089
  }
2090
2090
 
2091
+ // src/server/quota/adapter.ts
2092
+ var QuotaError = class extends Error {
2093
+ status;
2094
+ constructor(status) {
2095
+ const msg = "message" in status && status.message ? status.message : "";
2096
+ super(msg ? `${status.state}: ${msg}` : status.state);
2097
+ this.name = "QuotaError";
2098
+ this.status = status;
2099
+ }
2100
+ };
2101
+ var QuotaAdapterRegistry = class {
2102
+ adapters = /* @__PURE__ */ new Map();
2103
+ register(adapter) {
2104
+ this.adapters.set(adapter.provider, adapter);
2105
+ }
2106
+ get(provider) {
2107
+ return this.adapters.get(provider);
2108
+ }
2109
+ list() {
2110
+ return Array.from(this.adapters.values());
2111
+ }
2112
+ };
2113
+ function baseSnapshot(provider, displayName, opts = {}) {
2114
+ return {
2115
+ provider,
2116
+ displayName,
2117
+ planName: opts.planName,
2118
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2119
+ freshness: "live",
2120
+ windows: opts.windows ?? []
2121
+ };
2122
+ }
2123
+
2124
+ // src/server/quota/cache.ts
2125
+ var QuotaCache = class {
2126
+ constructor(ttlMs = 6e4) {
2127
+ this.ttlMs = ttlMs;
2128
+ }
2129
+ store = /* @__PURE__ */ new Map();
2130
+ /** Fresh cached snapshot, or null if expired / absent. */
2131
+ getFresh(provider) {
2132
+ const entry = this.store.get(provider);
2133
+ if (!entry) return null;
2134
+ if (Date.now() > entry.expiresAt) return null;
2135
+ return { ...entry.snapshot, freshness: "cached" };
2136
+ }
2137
+ /** Last successful snapshot regardless of TTL (for stale-while-revalidate). */
2138
+ getStale(provider) {
2139
+ const entry = this.store.get(provider);
2140
+ if (!entry) return null;
2141
+ return { ...entry.snapshot, freshness: "stale" };
2142
+ }
2143
+ set(snapshot) {
2144
+ this.store.set(snapshot.provider, {
2145
+ snapshot,
2146
+ expiresAt: Date.now() + this.ttlMs,
2147
+ updatedAt: Date.now()
2148
+ });
2149
+ }
2150
+ clear(provider) {
2151
+ if (provider) this.store.delete(provider);
2152
+ else this.store.clear();
2153
+ }
2154
+ };
2155
+
2156
+ // src/server/quota/schemas.ts
2157
+ var import_zod3 = require("zod");
2158
+ var QuotaWindowSchema = import_zod3.z.object({
2159
+ id: import_zod3.z.string(),
2160
+ label: import_zod3.z.string(),
2161
+ usedPercent: import_zod3.z.number().min(0).max(100).default(0),
2162
+ remainingPercent: import_zod3.z.number().min(0).max(100).default(0),
2163
+ used: import_zod3.z.number().optional(),
2164
+ limit: import_zod3.z.number().optional(),
2165
+ durationMins: import_zod3.z.number().optional(),
2166
+ resetsAt: import_zod3.z.string().optional(),
2167
+ isUnlimited: import_zod3.z.boolean().optional(),
2168
+ modelName: import_zod3.z.string().optional()
2169
+ });
2170
+ var QuotaProviderStatusSchema = import_zod3.z.object({
2171
+ state: import_zod3.z.enum([
2172
+ "ok",
2173
+ "auth_failed",
2174
+ "not_configured",
2175
+ "upstream_unavailable",
2176
+ "rate_limited",
2177
+ "malformed_response",
2178
+ "timed_out",
2179
+ "error"
2180
+ ]),
2181
+ message: import_zod3.z.string().optional(),
2182
+ category: import_zod3.z.string().optional()
2183
+ });
2184
+ var QuotaSnapshotSchema = import_zod3.z.object({
2185
+ provider: import_zod3.z.enum(["codex", "claude", "glm", "minimax", "kimi"]),
2186
+ displayName: import_zod3.z.string(),
2187
+ planName: import_zod3.z.string().optional(),
2188
+ fetchedAt: import_zod3.z.string(),
2189
+ freshness: import_zod3.z.enum(["live", "cached", "stale"]),
2190
+ windows: import_zod3.z.array(QuotaWindowSchema).default([]),
2191
+ status: QuotaProviderStatusSchema
2192
+ });
2193
+ var QuotaResponseSchema = import_zod3.z.object({
2194
+ providers: import_zod3.z.array(QuotaSnapshotSchema).default([])
2195
+ });
2196
+ function validateQuotaSnapshot(data) {
2197
+ return QuotaSnapshotSchema.parse(data);
2198
+ }
2199
+
2200
+ // src/server/quota/quotaService.ts
2201
+ var QuotaService = class {
2202
+ constructor(registry2, cache2 = new QuotaCache(), configuredCache = null, fetchTimeoutMs = 8e3) {
2203
+ this.registry = registry2;
2204
+ this.cache = cache2;
2205
+ this.configuredCache = configuredCache;
2206
+ this.fetchTimeoutMs = fetchTimeoutMs;
2207
+ }
2208
+ /** Cap concurrent upstream calls so a slow provider can't block the others. */
2209
+ fetchTimeoutMs;
2210
+ /** In-flight promises keyed by provider id, to dedupe concurrent requests. */
2211
+ inflight = /* @__PURE__ */ new Map();
2212
+ /**
2213
+ * List provider ids that are configured locally. Cheap (no network).
2214
+ * The dashboard only shows these — not-configured providers are excluded.
2215
+ */
2216
+ async discover() {
2217
+ const all = this.registry.list();
2218
+ const checks = await Promise.all(
2219
+ all.map(async (a) => ({ id: a.provider, configured: await safeIsConfigured(a) }))
2220
+ );
2221
+ return checks.filter((c) => c.configured).map((c) => c.id);
2222
+ }
2223
+ /**
2224
+ * Fetch one provider's snapshot. Fresh if available; stale-but-retained
2225
+ * on failure; never throws (errors become structured statuses).
2226
+ */
2227
+ async fetchOne(provider) {
2228
+ const fresh = this.cache.getFresh(provider);
2229
+ if (fresh) return fresh;
2230
+ const adapter = this.registry.get(provider);
2231
+ if (!adapter) return null;
2232
+ let p = this.inflight.get(provider);
2233
+ if (!p) {
2234
+ p = this.fetchWithTimeout(adapter).finally(() => this.inflight.delete(provider));
2235
+ this.inflight.set(provider, p);
2236
+ }
2237
+ return p;
2238
+ }
2239
+ /**
2240
+ * Fetch all configured providers concurrently. Partial success: one
2241
+ * provider's failure never breaks the others. Order = registry order.
2242
+ */
2243
+ async fetchAll() {
2244
+ const ids = this.configuredCache ?? await this.discover();
2245
+ const byId = /* @__PURE__ */ new Map();
2246
+ const snapshots = await Promise.all(ids.map((id) => this.fetchOne(id)));
2247
+ for (const adapter of this.registry.list()) {
2248
+ const snap = snapshots.find((s) => s?.provider === adapter.provider);
2249
+ if (snap) byId.set(adapter.provider, snap);
2250
+ }
2251
+ return { providers: this.registry.list().map((a) => byId.get(a.provider)).filter((s) => !!s) };
2252
+ }
2253
+ /** Force a refresh of all configured providers, bypassing the cache. */
2254
+ async refreshAll() {
2255
+ this.configuredCache?.forEach(() => {
2256
+ });
2257
+ for (const adapter of this.registry.list()) {
2258
+ this.cache.clear(adapter.provider);
2259
+ }
2260
+ return this.fetchAll();
2261
+ }
2262
+ /**
2263
+ * Validate a credential without caching it or writing it to disk. This keeps
2264
+ * the settings form transactional: only credentials accepted upstream are
2265
+ * persisted by the native app.
2266
+ */
2267
+ async validateCredential(provider, credential) {
2268
+ const adapter = this.registry.get(provider);
2269
+ if (!adapter) {
2270
+ return {
2271
+ provider,
2272
+ valid: false,
2273
+ status: { state: "not_configured", message: "Unsupported provider" }
2274
+ };
2275
+ }
2276
+ try {
2277
+ const snapshot = await withTimeout(
2278
+ adapter.fetch({ credential }),
2279
+ this.fetchTimeoutMs,
2280
+ provider
2281
+ );
2282
+ const validated = validateQuotaSnapshot(snapshot);
2283
+ return { provider, valid: validated.status.state === "ok", status: validated.status };
2284
+ } catch (err) {
2285
+ return { provider, valid: false, status: statusForError(err, this.fetchTimeoutMs) };
2286
+ }
2287
+ }
2288
+ async fetchWithTimeout(adapter) {
2289
+ try {
2290
+ const snapshot = await withTimeout(adapter.fetch(), this.fetchTimeoutMs, adapter.provider);
2291
+ const validated = validateQuotaSnapshot(snapshot);
2292
+ this.cache.set(validated);
2293
+ return validated;
2294
+ } catch (err) {
2295
+ return this.handleFailure(adapter, err);
2296
+ }
2297
+ }
2298
+ handleFailure(adapter, err) {
2299
+ const status = statusForError(err, this.fetchTimeoutMs);
2300
+ const stale = this.cache.getStale(adapter.provider);
2301
+ if (stale) {
2302
+ return { ...stale, freshness: "stale", status };
2303
+ }
2304
+ return {
2305
+ provider: adapter.provider,
2306
+ displayName: adapter.displayName,
2307
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2308
+ freshness: "stale",
2309
+ windows: [],
2310
+ status
2311
+ };
2312
+ }
2313
+ };
2314
+ function statusForError(err, timeoutMs) {
2315
+ if (err instanceof QuotaError) return err.status;
2316
+ if (err instanceof TimeoutError) {
2317
+ return { state: "timed_out", message: `upstream did not respond within ${timeoutMs}ms` };
2318
+ }
2319
+ return { state: "error", message: redact(err), category: "unexpected" };
2320
+ }
2321
+ var TimeoutError = class extends Error {
2322
+ constructor(provider) {
2323
+ super(`quota fetch timed out: ${provider}`);
2324
+ this.name = "TimeoutError";
2325
+ }
2326
+ };
2327
+ function withTimeout(p, ms, provider) {
2328
+ return new Promise((resolve2, reject) => {
2329
+ const timer = setTimeout(() => reject(new TimeoutError(provider)), ms);
2330
+ p.then(
2331
+ (v) => {
2332
+ clearTimeout(timer);
2333
+ resolve2(v);
2334
+ },
2335
+ (e) => {
2336
+ clearTimeout(timer);
2337
+ reject(e);
2338
+ }
2339
+ );
2340
+ });
2341
+ }
2342
+ async function safeIsConfigured(adapter) {
2343
+ try {
2344
+ return await adapter.isConfigured();
2345
+ } catch {
2346
+ return false;
2347
+ }
2348
+ }
2349
+ function redact(err) {
2350
+ const msg = err instanceof Error ? err.message : String(err);
2351
+ return msg.replace(/(sk-[A-Za-z0-9_-]{6,})[A-Za-z0-9_-]*/g, "$1\u2026").replace(/(Bearer\s+)[A-Za-z0-9._-]+/gi, "$1\u2026").slice(0, 200);
2352
+ }
2353
+
2354
+ // src/server/quota/adapters/codex.ts
2355
+ var import_node_fs8 = require("node:fs");
2356
+ var import_node_path8 = require("node:path");
2357
+ var import_node_os8 = require("node:os");
2358
+ var import_node_child_process2 = require("node:child_process");
2359
+
2360
+ // src/server/quota/helpers.ts
2361
+ function unixToIso(unix) {
2362
+ if (unix === null || unix === void 0 || unix === "") return void 0;
2363
+ const n = typeof unix === "string" ? parseInt(unix, 10) : unix;
2364
+ if (!Number.isFinite(n) || n <= 0) return void 0;
2365
+ const ms = n > 1e12 ? n : n * 1e3;
2366
+ return new Date(ms).toISOString();
2367
+ }
2368
+ function clampPercent(v) {
2369
+ if (!Number.isFinite(v)) return 0;
2370
+ return Math.max(0, Math.min(100, v));
2371
+ }
2372
+ function round1(v) {
2373
+ return Math.round(v * 10) / 10;
2374
+ }
2375
+ function windowFromPercent(id, label, usedPercent, opts = {}) {
2376
+ const used = round1(clampPercent(usedPercent));
2377
+ return {
2378
+ id,
2379
+ label,
2380
+ usedPercent: used,
2381
+ remainingPercent: round1(100 - used),
2382
+ durationMins: opts.durationMins,
2383
+ resetsAt: opts.resetsAt,
2384
+ used: opts.used,
2385
+ limit: opts.limit,
2386
+ modelName: opts.modelName,
2387
+ isUnlimited: opts.isUnlimited
2388
+ };
2389
+ }
2390
+ function windowFromCounts(id, label, used, limit, opts = {}) {
2391
+ if (limit <= 0) {
2392
+ return { id, label, usedPercent: 0, remainingPercent: 100, used, limit, isUnlimited: true, ...opts };
2393
+ }
2394
+ const pct = round1(clampPercent(used / limit * 100));
2395
+ return {
2396
+ id,
2397
+ label,
2398
+ usedPercent: pct,
2399
+ remainingPercent: round1(100 - pct),
2400
+ used,
2401
+ limit,
2402
+ ...opts
2403
+ };
2404
+ }
2405
+ async function fetchJsonWithTimeout(url, opts = {}) {
2406
+ const ctrl = new AbortController();
2407
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 8e3);
2408
+ try {
2409
+ const res = await fetch(url, { headers: opts.headers, signal: ctrl.signal });
2410
+ if (!res.ok) {
2411
+ const body = await res.text().catch(() => "");
2412
+ throw new HttpError(res.status, body.slice(0, 200));
2413
+ }
2414
+ return await res.json();
2415
+ } finally {
2416
+ clearTimeout(timer);
2417
+ }
2418
+ }
2419
+ var HttpError = class extends Error {
2420
+ constructor(status, body) {
2421
+ super(`HTTP ${status}`);
2422
+ this.status = status;
2423
+ this.body = body;
2424
+ this.name = "HttpError";
2425
+ }
2426
+ };
2427
+ function classifyHttpError(err) {
2428
+ if (err.status === 401 || err.status === 403) {
2429
+ return { state: "auth_failed", message: "credential rejected by provider" };
2430
+ }
2431
+ if (err.status === 429) {
2432
+ return { state: "rate_limited", message: "provider throttled the request" };
2433
+ }
2434
+ return { state: "upstream_unavailable", message: `provider returned HTTP ${err.status}` };
2435
+ }
2436
+
2437
+ // src/server/quota/adapters/codex.ts
2438
+ function codexHome() {
2439
+ return process.env.CODEX_HOME || (0, import_node_path8.join)((0, import_node_os8.homedir)(), ".codex");
2440
+ }
2441
+ var codexAdapter = {
2442
+ provider: "codex",
2443
+ displayName: "OpenAI Codex",
2444
+ async isConfigured() {
2445
+ return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(codexHome(), "auth.json")) && resolveCodexBinary() !== null;
2446
+ },
2447
+ async fetch() {
2448
+ const result = await queryRateLimits();
2449
+ const buckets = result.rateLimitsByLimitId ?? (result.rateLimits ? { primary: result.rateLimits } : {});
2450
+ const windows = [];
2451
+ for (const [key, bucket] of Object.entries(buckets)) {
2452
+ if (bucket.primary) {
2453
+ windows.push(windowFromPercent(`codex_${key}_primary`, labelForBucket(key, "primary", bucket), bucket.primary.usedPercent ?? 0, {
2454
+ durationMins: bucket.primary.windowDurationMins,
2455
+ resetsAt: unixToIso(bucket.primary.resetsAt)
2456
+ }));
2457
+ }
2458
+ if (bucket.secondary) {
2459
+ windows.push(windowFromPercent(`codex_${key}_secondary`, labelForBucket(key, "secondary", bucket), bucket.secondary.usedPercent ?? 0, {
2460
+ durationMins: bucket.secondary.windowDurationMins,
2461
+ resetsAt: unixToIso(bucket.secondary.resetsAt)
2462
+ }));
2463
+ }
2464
+ }
2465
+ const snap = baseSnapshot("codex", "OpenAI Codex", {
2466
+ planName: result.planType ? capitalize(result.planType) : void 0,
2467
+ windows
2468
+ });
2469
+ return { ...snap, status: { state: "ok" } };
2470
+ }
2471
+ };
2472
+ function labelForBucket(key, tier, bucket) {
2473
+ const dur = tier === "primary" ? bucket.primary?.windowDurationMins : bucket.secondary?.windowDurationMins;
2474
+ const durLabel = dur ? durationLabel(dur) : capitalize(tier);
2475
+ const who = bucket.limitName || (key && key !== "primary" ? key : "");
2476
+ return who ? `${capitalize(who)} \xB7 ${durLabel}` : durLabel;
2477
+ }
2478
+ function durationLabel(mins) {
2479
+ if (mins >= 10080) return "Weekly";
2480
+ if (mins >= 1440) return `${Math.round(mins / 1440)}-Day`;
2481
+ if (mins >= 60) return `${Math.round(mins / 60)}-Hour`;
2482
+ return `${mins}m`;
2483
+ }
2484
+ function capitalize(s) {
2485
+ return s.charAt(0).toUpperCase() + s.slice(1);
2486
+ }
2487
+ async function queryRateLimits() {
2488
+ const codexBinary = resolveCodexBinary();
2489
+ if (!codexBinary) {
2490
+ throw new QuotaError({ state: "not_configured", message: "official Codex CLI not found" });
2491
+ }
2492
+ const binaryDir = (0, import_node_path8.dirname)(codexBinary);
2493
+ const childPath = [binaryDir, process.env.PATH].filter(Boolean).join(":");
2494
+ const proc = (0, import_node_child_process2.spawn)(codexBinary, ["app-server"], {
2495
+ stdio: ["pipe", "pipe", "pipe"],
2496
+ env: { ...process.env, PATH: childPath }
2497
+ });
2498
+ const client = new JsonRpcClient(proc);
2499
+ try {
2500
+ await client.request("initialize", {
2501
+ protocolVersion: "2025-03-26",
2502
+ clientInfo: { name: "tokendash", version: "1.0.0" }
2503
+ });
2504
+ client.notify("initialized", {});
2505
+ const res = await client.request("account/rateLimits/read", {});
2506
+ return res;
2507
+ } catch (err) {
2508
+ throw toQuotaError(err);
2509
+ } finally {
2510
+ client.dispose();
2511
+ try {
2512
+ proc.kill("SIGKILL");
2513
+ } catch {
2514
+ }
2515
+ }
2516
+ }
2517
+ function toQuotaError(err) {
2518
+ const msg = err instanceof Error ? err.message : String(err);
2519
+ if (/not found|ENOENT|spawn/i.test(msg)) {
2520
+ return new QuotaError({ state: "not_configured", message: "codex app-server unavailable" });
2521
+ }
2522
+ if (/401|403|unauthor|auth/i.test(msg)) {
2523
+ return new QuotaError({ state: "auth_failed", message: "codex session not authenticated" });
2524
+ }
2525
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2526
+ }
2527
+ var JsonRpcClient = class {
2528
+ constructor(proc) {
2529
+ this.proc = proc;
2530
+ proc.stdout.setEncoding("utf8");
2531
+ proc.stdout.on("data", (chunk) => this.onData(chunk));
2532
+ proc.on("error", (err) => this.failAll(err));
2533
+ proc.on("close", () => this.failAll(new Error("codex app-server closed unexpectedly")));
2534
+ }
2535
+ id = 0;
2536
+ buffer = "";
2537
+ pending = /* @__PURE__ */ new Map();
2538
+ disposed = false;
2539
+ request(method, params) {
2540
+ if (this.disposed) return Promise.reject(new Error("client disposed"));
2541
+ const id = ++this.id;
2542
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
2543
+ return new Promise((resolve2, reject) => {
2544
+ this.pending.set(id, { resolve: resolve2, reject });
2545
+ this.proc.stdin.write(msg, (err) => {
2546
+ if (err) reject(err instanceof Error ? err : new Error(String(err)));
2547
+ });
2548
+ });
2549
+ }
2550
+ notify(method, params) {
2551
+ if (this.disposed) return;
2552
+ const msg = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
2553
+ this.proc.stdin.write(msg, () => {
2554
+ });
2555
+ }
2556
+ dispose() {
2557
+ this.disposed = true;
2558
+ this.failAll(new Error("disposed"));
2559
+ }
2560
+ onData(chunk) {
2561
+ this.buffer += chunk;
2562
+ let idx;
2563
+ while ((idx = this.buffer.indexOf("\n")) >= 0) {
2564
+ const line = this.buffer.slice(0, idx).trim();
2565
+ this.buffer = this.buffer.slice(idx + 1);
2566
+ if (!line) continue;
2567
+ let msg;
2568
+ try {
2569
+ msg = JSON.parse(line);
2570
+ } catch {
2571
+ continue;
2572
+ }
2573
+ if (msg.id === void 0) continue;
2574
+ const entry = this.pending.get(msg.id);
2575
+ if (!entry) continue;
2576
+ this.pending.delete(msg.id);
2577
+ if (msg.error) entry.reject(new Error(msg.error.message || "codex JSON-RPC error"));
2578
+ else entry.resolve(msg.result);
2579
+ }
2580
+ }
2581
+ failAll(err) {
2582
+ for (const [, entry] of this.pending) entry.reject(err);
2583
+ this.pending.clear();
2584
+ }
2585
+ };
2586
+ function resolveCodexBinary(options = {}) {
2587
+ const home = options.home ?? (0, import_node_os8.homedir)();
2588
+ const path = options.path ?? process.env.PATH ?? "";
2589
+ const explicitBinary = options.explicitBinary ?? process.env.CODEX_BIN;
2590
+ const isExecutable = options.isExecutable ?? defaultIsExecutable;
2591
+ const nvmRoot = (0, import_node_path8.join)(home, ".nvm", "versions", "node");
2592
+ const nvmVersions = options.nvmVersions ?? readDirectoryNames(nvmRoot);
2593
+ const candidates = [
2594
+ explicitBinary,
2595
+ "/Applications/Codex.app/Contents/Resources/codex",
2596
+ (0, import_node_path8.join)(home, "Applications", "Codex.app", "Contents", "Resources", "codex"),
2597
+ "/opt/homebrew/bin/codex",
2598
+ "/usr/local/bin/codex",
2599
+ (0, import_node_path8.join)(home, ".local", "bin", "codex"),
2600
+ (0, import_node_path8.join)(home, ".volta", "bin", "codex"),
2601
+ (0, import_node_path8.join)(home, ".bun", "bin", "codex"),
2602
+ ...nvmVersions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true })).map((version) => (0, import_node_path8.join)(nvmRoot, version, "bin", "codex")),
2603
+ ...path.split(":").filter(Boolean).map((directory) => (0, import_node_path8.join)(directory, "codex"))
2604
+ ];
2605
+ for (const candidate of candidates) {
2606
+ if (candidate && isExecutable(candidate)) return candidate;
2607
+ }
2608
+ return null;
2609
+ }
2610
+ function defaultIsExecutable(candidate) {
2611
+ try {
2612
+ (0, import_node_fs8.accessSync)(candidate, import_node_fs8.constants.X_OK);
2613
+ return true;
2614
+ } catch {
2615
+ return false;
2616
+ }
2617
+ }
2618
+ function readDirectoryNames(directory) {
2619
+ try {
2620
+ return (0, import_node_fs8.readdirSync)(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2621
+ } catch {
2622
+ return [];
2623
+ }
2624
+ }
2625
+
2626
+ // src/server/quota/adapters/claude.ts
2627
+ var import_node_fs9 = require("node:fs");
2628
+ var import_node_path9 = require("node:path");
2629
+ var import_node_os9 = require("node:os");
2630
+ var import_node_child_process3 = require("node:child_process");
2631
+ var import_node_crypto = require("node:crypto");
2632
+ var claudeAdapter = {
2633
+ provider: "claude",
2634
+ displayName: "Claude Code",
2635
+ async isConfigured() {
2636
+ const token = readClaudeToken();
2637
+ return !!token;
2638
+ },
2639
+ async fetch() {
2640
+ const token = readClaudeToken();
2641
+ if (!token) {
2642
+ throw new QuotaError({ state: "not_configured", message: "no Claude Code OAuth credential found" });
2643
+ }
2644
+ let data;
2645
+ try {
2646
+ data = await fetchJsonWithTimeout("https://api.anthropic.com/api/oauth/usage", {
2647
+ headers: {
2648
+ Authorization: `Bearer ${token}`,
2649
+ "anthropic-beta": "oauth-2025-04-20",
2650
+ "Content-Type": "application/json"
2651
+ }
2652
+ });
2653
+ } catch (err) {
2654
+ throw classifyFetchError(err);
2655
+ }
2656
+ const windows = [];
2657
+ if (data.five_hour) {
2658
+ windows.push(windowFromPercent("five_hour", "5-Hour Window", data.five_hour.utilization ?? 0, {
2659
+ durationMins: 300,
2660
+ resetsAt: normalizeIso(data.five_hour.resets_at)
2661
+ }));
2662
+ }
2663
+ if (data.seven_day) {
2664
+ windows.push(windowFromPercent("seven_day", "Weekly", data.seven_day.utilization ?? 0, {
2665
+ durationMins: 10080,
2666
+ resetsAt: normalizeIso(data.seven_day.resets_at)
2667
+ }));
2668
+ }
2669
+ if (data.seven_day_opus?.utilization !== void 0 && data.seven_day_opus?.utilization !== null) {
2670
+ windows.push(windowFromPercent("seven_day_opus", "Weekly \xB7 Opus", data.seven_day_opus.utilization, {
2671
+ durationMins: 10080,
2672
+ resetsAt: normalizeIso(data.seven_day_opus.resets_at)
2673
+ }));
2674
+ }
2675
+ const snap = baseSnapshot("claude", "Claude Code", { windows });
2676
+ return { ...snap, status: { state: "ok" } };
2677
+ }
2678
+ };
2679
+ function classifyFetchError(err) {
2680
+ if (err instanceof HttpError) {
2681
+ const c = classifyHttpError(err);
2682
+ return new QuotaError(c);
2683
+ }
2684
+ const msg = err instanceof Error ? err.message : String(err);
2685
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2686
+ }
2687
+ function normalizeIso(s) {
2688
+ return s ? new Date(s).toISOString() : void 0;
2689
+ }
2690
+ function readClaudeToken() {
2691
+ if (process.platform === "darwin") {
2692
+ const token = readFromKeychain();
2693
+ if (token) return token;
2694
+ }
2695
+ const configDir = process.env.CLAUDE_CONFIG_DIR || (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".claude");
2696
+ const credPath = (0, import_node_path9.join)(configDir, ".credentials.json");
2697
+ if ((0, import_node_fs9.existsSync)(credPath)) {
2698
+ try {
2699
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)(credPath, "utf8"));
2700
+ return extractClaudeAccessToken(parsed);
2701
+ } catch {
2702
+ return null;
2703
+ }
2704
+ }
2705
+ return null;
2706
+ }
2707
+ function readFromKeychain() {
2708
+ const candidates = claudeKeychainServiceNames(process.env.CLAUDE_CONFIG_DIR);
2709
+ try {
2710
+ const list = (0, import_node_child_process3.execFileSync)("security", ["dump-keychain"], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
2711
+ for (const m of list.matchAll(/"svce"<blob>="([^"]*Claude Code-credentials[^"]*)"/g)) {
2712
+ if (m[1] && !candidates.includes(m[1])) candidates.push(m[1]);
2713
+ }
2714
+ } catch {
2715
+ }
2716
+ const accounts = [safeUsername(), void 0];
2717
+ for (const name of candidates) {
2718
+ for (const account of accounts) {
2719
+ try {
2720
+ const args = ["find-generic-password", "-s", name];
2721
+ if (account) args.push("-a", account);
2722
+ args.push("-w");
2723
+ const raw = (0, import_node_child_process3.execFileSync)("/usr/bin/security", args, {
2724
+ stdio: ["ignore", "pipe", "ignore"],
2725
+ encoding: "utf8",
2726
+ timeout: 2e3
2727
+ }).trim();
2728
+ if (!raw) continue;
2729
+ const token = extractClaudeAccessToken(JSON.parse(raw));
2730
+ if (token) return token;
2731
+ } catch {
2732
+ continue;
2733
+ }
2734
+ }
2735
+ }
2736
+ return null;
2737
+ }
2738
+ function claudeKeychainServiceNames(configDir) {
2739
+ if (!configDir) return ["Claude Code-credentials"];
2740
+ const hash = (0, import_node_crypto.createHash)("sha256").update(configDir).digest("hex").slice(0, 8);
2741
+ return [`Claude Code-credentials-${hash}`, "Claude Code-credentials"];
2742
+ }
2743
+ function extractClaudeAccessToken(value) {
2744
+ if (!value || typeof value !== "object") return null;
2745
+ const root = value;
2746
+ const nested = root.claudeAiOauth;
2747
+ const credentials = nested && typeof nested === "object" ? nested : root;
2748
+ return typeof credentials.accessToken === "string" && credentials.accessToken ? credentials.accessToken : null;
2749
+ }
2750
+ function safeUsername() {
2751
+ try {
2752
+ return (0, import_node_os9.userInfo)().username?.trim() || void 0;
2753
+ } catch {
2754
+ return void 0;
2755
+ }
2756
+ }
2757
+
2758
+ // src/server/quota/adapters/glm.ts
2759
+ var import_node_fs11 = require("node:fs");
2760
+ var import_node_path11 = require("node:path");
2761
+ var import_node_os11 = require("node:os");
2762
+
2763
+ // src/server/quota/credentialsFile.ts
2764
+ var import_node_fs10 = require("node:fs");
2765
+ var import_node_path10 = require("node:path");
2766
+ var import_node_os10 = require("node:os");
2767
+ function readStoredCredential(provider) {
2768
+ try {
2769
+ const path = (0, import_node_path10.join)((0, import_node_os10.homedir)(), ".tokendash", "credentials.json");
2770
+ if (!(0, import_node_fs10.existsSync)(path)) return null;
2771
+ const all = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
2772
+ const entry = all?.[provider];
2773
+ if (entry && typeof entry === "object" && typeof entry.apiKey === "string") {
2774
+ const apiKey = entry.apiKey;
2775
+ if (!apiKey) return null;
2776
+ const baseUrl = entry.baseUrl;
2777
+ return { apiKey, baseUrl: typeof baseUrl === "string" ? baseUrl : void 0 };
2778
+ }
2779
+ return null;
2780
+ } catch {
2781
+ return null;
2782
+ }
2783
+ }
2784
+
2785
+ // src/server/quota/adapters/glm.ts
2786
+ var glmAdapter = {
2787
+ provider: "glm",
2788
+ displayName: "GLM Coding Plan",
2789
+ async isConfigured() {
2790
+ return !!resolveCredential();
2791
+ },
2792
+ async fetch(options) {
2793
+ const cred = resolveCredential(options?.credential);
2794
+ if (!cred) {
2795
+ throw new QuotaError({ state: "not_configured", message: "set ZAI_API_KEY or ZHIPU_API_KEY" });
2796
+ }
2797
+ let data;
2798
+ try {
2799
+ data = await fetchJsonWithTimeout(`${cred.base}/api/monitor/usage/quota/limit`, {
2800
+ headers: {
2801
+ // GLM wants the raw key, NOT "Bearer <key>".
2802
+ Authorization: cred.key,
2803
+ "Accept-Language": "en-US,en",
2804
+ "Content-Type": "application/json"
2805
+ }
2806
+ });
2807
+ } catch (err) {
2808
+ throw classifyFetchError2(err);
2809
+ }
2810
+ if (!data?.success && data?.code !== 200) {
2811
+ throw new QuotaError({ state: "upstream_unavailable", message: data?.msg || "GLM quota request failed" });
2812
+ }
2813
+ const limits = data.data?.limits ?? [];
2814
+ const windows = [];
2815
+ const tokenLimits = limits.filter((l) => l.type === "TOKENS_LIMIT" && typeof l.percentage === "number").sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
2816
+ tokenLimits.forEach((l, i) => {
2817
+ const isShort = i === 0;
2818
+ windows.push(windowFromPercent(
2819
+ isShort ? "glm_5h" : "glm_weekly",
2820
+ isShort ? "5-Hour Window" : "Weekly",
2821
+ l.percentage ?? 0,
2822
+ { durationMins: isShort ? 300 : 10080, resetsAt: unixToIso(l.nextResetTime) }
2823
+ ));
2824
+ });
2825
+ const timeLimit = limits.find((l) => l.type === "TIME_LIMIT");
2826
+ if (timeLimit && typeof timeLimit.usage === "number") {
2827
+ windows.push(windowFromCounts(
2828
+ "glm_mcp_monthly",
2829
+ "MCP \xB7 Monthly",
2830
+ timeLimit.currentValue ?? 0,
2831
+ timeLimit.usage
2832
+ ));
2833
+ }
2834
+ const snap = baseSnapshot("glm", "GLM Coding Plan", {
2835
+ planName: data.data?.level ? capitalize2(data.data.level) : void 0,
2836
+ windows
2837
+ });
2838
+ return { ...snap, status: { state: "ok" } };
2839
+ }
2840
+ };
2841
+ function classifyFetchError2(err) {
2842
+ if (err instanceof HttpError) {
2843
+ const c = classifyHttpError(err);
2844
+ return new QuotaError(c);
2845
+ }
2846
+ const msg = err instanceof Error ? err.message : String(err);
2847
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2848
+ }
2849
+ function resolveCredential(proposed) {
2850
+ if (proposed?.apiKey) {
2851
+ return {
2852
+ key: proposed.apiKey,
2853
+ base: proposed.baseUrl || "https://open.bigmodel.cn"
2854
+ };
2855
+ }
2856
+ const stored = readStoredCredential("glm");
2857
+ if (stored) return { key: stored.apiKey, base: stored.baseUrl || "https://open.bigmodel.cn" };
2858
+ const zai = envOrConfig("ZAI_API_KEY");
2859
+ if (zai) return { key: zai, base: envOrConfig("ZAI_BASE_URL") || "https://api.z.ai" };
2860
+ const zhipu = envOrConfig("ZHIPU_API_KEY");
2861
+ if (zhipu) return { key: zhipu, base: envOrConfig("ZHIPU_BASE_URL") || "https://open.bigmodel.cn" };
2862
+ const anthropicBase = envOrConfig("ANTHROPIC_BASE_URL");
2863
+ if (anthropicBase && isGlmHost(anthropicBase)) {
2864
+ const origin = originOf(anthropicBase);
2865
+ const token = envOrConfig("ANTHROPIC_AUTH_TOKEN") || envOrConfig("ANTHROPIC_API_KEY");
2866
+ if (origin && token) return { key: token, base: origin };
2867
+ }
2868
+ return null;
2869
+ }
2870
+ function isGlmHost(url) {
2871
+ const h = url.toLowerCase();
2872
+ return h.includes("bigmodel.cn") || h.includes("z.ai");
2873
+ }
2874
+ function originOf(url) {
2875
+ try {
2876
+ const u = new URL(url);
2877
+ return `${u.protocol}//${u.host}`;
2878
+ } catch {
2879
+ return null;
2880
+ }
2881
+ }
2882
+ var claudeSettingsEnv;
2883
+ function envOrConfig(key) {
2884
+ if (process.env[key]) return process.env[key];
2885
+ if (claudeSettingsEnv === void 0) claudeSettingsEnv = loadClaudeSettingsEnv();
2886
+ return claudeSettingsEnv?.[key];
2887
+ }
2888
+ function loadClaudeSettingsEnv() {
2889
+ const configDir = process.env.CLAUDE_CONFIG_DIR || (0, import_node_path11.join)((0, import_node_os11.homedir)(), ".claude");
2890
+ try {
2891
+ const path = (0, import_node_path11.join)(configDir, "settings.json");
2892
+ if (!(0, import_node_fs11.existsSync)(path)) return null;
2893
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
2894
+ return parsed?.env && typeof parsed.env === "object" ? parsed.env : null;
2895
+ } catch {
2896
+ return null;
2897
+ }
2898
+ }
2899
+ function capitalize2(s) {
2900
+ return s.charAt(0).toUpperCase() + s.slice(1);
2901
+ }
2902
+
2903
+ // src/server/quota/adapters/minimax.ts
2904
+ var minimaxAdapter = {
2905
+ provider: "minimax",
2906
+ displayName: "MiniMax Coding Plan",
2907
+ async isConfigured() {
2908
+ return !!resolveCredential2();
2909
+ },
2910
+ async fetch(options) {
2911
+ const cred = resolveCredential2(options?.credential);
2912
+ if (!cred) {
2913
+ throw new QuotaError({ state: "not_configured", message: "set MINIMAX_API_KEY (Subscription Key)" });
2914
+ }
2915
+ let data;
2916
+ try {
2917
+ data = await fetchJsonWithTimeout(`${cred.base}/v1/token_plan/remains`, {
2918
+ headers: {
2919
+ Authorization: `Bearer ${cred.key}`,
2920
+ "Content-Type": "application/json"
2921
+ }
2922
+ });
2923
+ } catch (err) {
2924
+ throw classifyFetchError3(err);
2925
+ }
2926
+ if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
2927
+ throw new QuotaError({
2928
+ state: "upstream_unavailable",
2929
+ message: data.base_resp.status_msg || `MiniMax error ${data.base_resp.status_code}`
2930
+ });
2931
+ }
2932
+ const windows = [];
2933
+ for (const m of data.model_remains ?? []) {
2934
+ const model = m.model_name ?? "MiniMax";
2935
+ const intervalTotal = m.current_interval_total_count ?? 0;
2936
+ const intervalRemaining = m.current_interval_usage_count ?? 0;
2937
+ if (intervalTotal > 0) {
2938
+ windows.push(windowFromCounts(
2939
+ `minimax_5h_${model}`,
2940
+ `5-Hour \xB7 ${model}`,
2941
+ Math.max(0, intervalTotal - intervalRemaining),
2942
+ intervalTotal,
2943
+ { durationMins: 300, resetsAt: unixToIso(m.end_time) }
2944
+ ));
2945
+ }
2946
+ const weeklyTotal = m.current_weekly_total_count ?? 0;
2947
+ const weeklyRemaining = m.current_weekly_usage_count ?? 0;
2948
+ if (weeklyTotal > 0) {
2949
+ windows.push(windowFromCounts(
2950
+ `minimax_weekly_${model}`,
2951
+ `Weekly \xB7 ${model}`,
2952
+ Math.max(0, weeklyTotal - weeklyRemaining),
2953
+ weeklyTotal,
2954
+ { durationMins: 10080 }
2955
+ ));
2956
+ }
2957
+ }
2958
+ const snap = baseSnapshot("minimax", "MiniMax Coding Plan", { windows });
2959
+ return { ...snap, status: { state: "ok" } };
2960
+ }
2961
+ };
2962
+ function classifyFetchError3(err) {
2963
+ if (err instanceof HttpError) {
2964
+ const c = classifyHttpError(err);
2965
+ return new QuotaError(c);
2966
+ }
2967
+ const msg = err instanceof Error ? err.message : String(err);
2968
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2969
+ }
2970
+ function resolveCredential2(proposed) {
2971
+ if (proposed?.apiKey) {
2972
+ const region2 = (process.env.MINIMAX_REGION || "").toLowerCase();
2973
+ const base2 = proposed.baseUrl || (region2 === "cn" ? "https://www.minimaxi.com" : "https://www.minimax.io");
2974
+ return { key: proposed.apiKey, base: base2 };
2975
+ }
2976
+ const stored = readStoredCredential("minimax");
2977
+ if (stored) {
2978
+ const region2 = (process.env.MINIMAX_REGION || "").toLowerCase();
2979
+ const base2 = stored.baseUrl || (region2 === "cn" ? "https://www.minimaxi.com" : "https://www.minimax.io");
2980
+ return { key: stored.apiKey, base: base2 };
2981
+ }
2982
+ const key = process.env.MINIMAX_API_KEY || process.env.MINIMAX_SUBSCRIPTION_KEY;
2983
+ if (!key) return null;
2984
+ const region = (process.env.MINIMAX_REGION || "").toLowerCase();
2985
+ const base = region === "cn" ? process.env.MINIMAX_BASE_URL || "https://www.minimaxi.com" : process.env.MINIMAX_BASE_URL || "https://www.minimax.io";
2986
+ return { key, base };
2987
+ }
2988
+
2989
+ // src/server/quota/adapters/kimi.ts
2990
+ var import_node_fs12 = require("node:fs");
2991
+ var import_node_path12 = require("node:path");
2992
+ var import_node_os12 = require("node:os");
2993
+ var KIMI_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
2994
+ var KIMI_BASE = "https://api.kimi.com/coding/v1";
2995
+ var KIMI_AUTH = "https://auth.kimi.com/api/oauth/token";
2996
+ var kimiAdapter = {
2997
+ provider: "kimi",
2998
+ displayName: "Kimi Code",
2999
+ async isConfigured() {
3000
+ const cred = readCredentials();
3001
+ return !!cred && !!cred.access_token;
3002
+ },
3003
+ async fetch(options) {
3004
+ const credPath = credentialsPath();
3005
+ let cred = options?.credential?.apiKey ? { access_token: options.credential.apiKey, token_type: "Bearer" } : readCredentials();
3006
+ if (!cred || !cred.access_token) {
3007
+ throw new QuotaError({ state: "not_configured", message: "run `kimi` to log in first" });
3008
+ }
3009
+ const nowSec = Math.floor(Date.now() / 1e3);
3010
+ if (cred.expires_at && cred.expires_at - nowSec < 300 && cred.refresh_token) {
3011
+ cred = await refreshToken(credPath, cred.refresh_token).catch(() => cred);
3012
+ }
3013
+ let data;
3014
+ try {
3015
+ data = await fetchJsonWithTimeout(`${KIMI_BASE}/usages`, {
3016
+ headers: {
3017
+ Authorization: `Bearer ${cred.access_token}`,
3018
+ Accept: "application/json"
3019
+ }
3020
+ });
3021
+ } catch (err) {
3022
+ throw classifyFetchError4(err);
3023
+ }
3024
+ const windows = [];
3025
+ if (data.usage) {
3026
+ const { used, limit } = toCounts(data.usage);
3027
+ if (limit > 0) {
3028
+ windows.push(windowFromCounts("kimi_weekly", "Weekly", used, limit, {
3029
+ durationMins: 10080,
3030
+ resetsAt: normalizeIso2(data.usage.resetTime)
3031
+ }));
3032
+ }
3033
+ }
3034
+ for (let i = 0; i < (data.limits?.length ?? 0); i++) {
3035
+ const entry = data.limits[i];
3036
+ const detail = entry?.detail;
3037
+ if (!detail) continue;
3038
+ const { used, limit } = toCounts(detail);
3039
+ if (limit <= 0) continue;
3040
+ const mins = minutesForWindow(entry?.window);
3041
+ windows.push(windowFromCounts(`kimi_limit_${i}`, mins ? `${durationLabel2(mins)} Window` : `Window ${i + 1}`, used, limit, {
3042
+ durationMins: mins,
3043
+ resetsAt: normalizeIso2(detail.resetTime)
3044
+ }));
3045
+ }
3046
+ const snap = baseSnapshot("kimi", "Kimi Code", {
3047
+ planName: data.user?.membership?.level ? prettifyLevel(data.user.membership.level) : void 0,
3048
+ windows
3049
+ });
3050
+ return { ...snap, status: { state: "ok" } };
3051
+ }
3052
+ };
3053
+ function classifyFetchError4(err) {
3054
+ if (err instanceof HttpError) {
3055
+ const c = classifyHttpError(err);
3056
+ return new QuotaError(c);
3057
+ }
3058
+ const msg = err instanceof Error ? err.message : String(err);
3059
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
3060
+ }
3061
+ function toCounts(detail) {
3062
+ const limit = toNumber(detail.limit);
3063
+ const remaining = toNumber(detail.remaining);
3064
+ if (limit <= 0) return { used: 0, limit: 0 };
3065
+ return { used: Math.max(0, limit - remaining), limit };
3066
+ }
3067
+ function toNumber(v) {
3068
+ if (v === void 0 || v === null) return 0;
3069
+ const n = typeof v === "string" ? parseFloat(v) : v;
3070
+ return Number.isFinite(n) ? n : 0;
3071
+ }
3072
+ function minutesForWindow(window) {
3073
+ if (!window?.duration) return void 0;
3074
+ const unit = (window.timeUnit || "").toUpperCase();
3075
+ if (unit.includes("HOUR")) return window.duration * 60;
3076
+ if (unit.includes("DAY")) return window.duration * 1440;
3077
+ return window.duration;
3078
+ }
3079
+ function durationLabel2(mins) {
3080
+ if (mins >= 10080) return "Weekly";
3081
+ if (mins >= 1440) return `${Math.round(mins / 1440)}-Day`;
3082
+ if (mins >= 60) return `${Math.round(mins / 60)}-Hour`;
3083
+ return `${mins}m`;
3084
+ }
3085
+ function normalizeIso2(s) {
3086
+ return s ? new Date(s).toISOString() : void 0;
3087
+ }
3088
+ function prettifyLevel(level) {
3089
+ return level.replace(/^LEVEL_/, "").toLowerCase().replace(/(^|_)(\w)/g, (_, __, c) => " " + c.toUpperCase()).trim();
3090
+ }
3091
+ function kimiDataDir() {
3092
+ return process.env.KIMI_DATA_DIR || (0, import_node_path12.join)((0, import_node_os12.homedir)(), ".kimi");
3093
+ }
3094
+ function credentialsPath() {
3095
+ return (0, import_node_path12.join)(kimiDataDir(), "credentials", "kimi-code.json");
3096
+ }
3097
+ function readCredentials() {
3098
+ const stored = readStoredCredential("kimi");
3099
+ if (stored?.apiKey) {
3100
+ return { access_token: stored.apiKey, token_type: "Bearer" };
3101
+ }
3102
+ const path = credentialsPath();
3103
+ if (!(0, import_node_fs12.existsSync)(path)) return null;
3104
+ try {
3105
+ return JSON.parse((0, import_node_fs12.readFileSync)(path, "utf8"));
3106
+ } catch {
3107
+ return null;
3108
+ }
3109
+ }
3110
+ async function refreshToken(credPath, refreshToken2) {
3111
+ const body = new URLSearchParams({
3112
+ client_id: KIMI_CLIENT_ID,
3113
+ grant_type: "refresh_token",
3114
+ refresh_token: refreshToken2
3115
+ });
3116
+ const res = await fetch(KIMI_AUTH, {
3117
+ method: "POST",
3118
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3119
+ body
3120
+ });
3121
+ if (!res.ok) throw new Error(`token refresh failed: HTTP ${res.status}`);
3122
+ const tokens = await res.json();
3123
+ const updated = {
3124
+ access_token: tokens.access_token,
3125
+ refresh_token: tokens.refresh_token || refreshToken2,
3126
+ expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 3600),
3127
+ token_type: tokens.token_type || "Bearer"
3128
+ };
3129
+ try {
3130
+ (0, import_node_fs12.writeFileSync)(credPath, JSON.stringify(updated, null, 2), "utf8");
3131
+ } catch {
3132
+ }
3133
+ return updated;
3134
+ }
3135
+
3136
+ // src/server/quota/index.ts
3137
+ var registry = new QuotaAdapterRegistry();
3138
+ registry.register(claudeAdapter);
3139
+ registry.register(codexAdapter);
3140
+ registry.register(glmAdapter);
3141
+ registry.register(minimaxAdapter);
3142
+ registry.register(kimiAdapter);
3143
+ var quotaCache = new QuotaCache();
3144
+ var quotaService = new QuotaService(registry, quotaCache);
3145
+
2091
3146
  // src/server/routes/api.ts
3147
+ async function getQuota(_req, res) {
3148
+ const force = _req.query.refresh === "1" || _req.query.refresh === "true";
3149
+ try {
3150
+ const data = force ? await quotaService.refreshAll() : await quotaService.fetchAll();
3151
+ res.json(data);
3152
+ } catch (error) {
3153
+ const message = error instanceof Error ? error.message : "Unknown error";
3154
+ res.status(500).json({ error: "Failed to fetch quota", hint: message });
3155
+ }
3156
+ }
3157
+ var editableQuotaProviders = /* @__PURE__ */ new Set(["glm", "kimi", "minimax"]);
3158
+ async function validateQuotaCredential(req, res) {
3159
+ const provider = req.body?.provider;
3160
+ const apiKey = typeof req.body?.apiKey === "string" ? req.body.apiKey.trim() : "";
3161
+ const baseUrl = typeof req.body?.baseUrl === "string" ? req.body.baseUrl.trim() : void 0;
3162
+ if (!editableQuotaProviders.has(provider) || !apiKey) {
3163
+ res.status(400).json({
3164
+ error: "Invalid credential request",
3165
+ hint: "A supported provider and non-empty token are required."
3166
+ });
3167
+ return;
3168
+ }
3169
+ const result = await quotaService.validateCredential(provider, {
3170
+ apiKey,
3171
+ baseUrl: baseUrl || void 0
3172
+ });
3173
+ res.status(result.valid ? 200 : 422).json(result);
3174
+ }
2092
3175
  function getAgents(_req, res) {
2093
3176
  try {
2094
3177
  const agents = detectAvailableAgents();
@@ -2121,10 +3204,11 @@ function registerApiRoutes(router, appInfo) {
2121
3204
  router.get("/projects", getProjects);
2122
3205
  router.get("/blocks", getBlocks);
2123
3206
  router.get("/analytics", getAnalytics);
3207
+ router.get("/quota", getQuota);
3208
+ router.post("/quota/validate", validateQuotaCredential);
2124
3209
  }
2125
3210
 
2126
3211
  // src/server/index.ts
2127
- var import_open = __toESM(require("open"), 1);
2128
3212
  var CLI_USAGE = [
2129
3213
  "Usage:",
2130
3214
  " tokendash",
@@ -2135,16 +3219,16 @@ var CLI_USAGE = [
2135
3219
  var PACKAGE_NAME = "@zhangferry-dev/tokendash";
2136
3220
  function getPackageVersion() {
2137
3221
  const __filename = (0, import_node_url.fileURLToPath)(__esbuild_import_meta_url);
2138
- const __dirname = (0, import_node_path8.dirname)(__filename);
3222
+ const __dirname = (0, import_node_path13.dirname)(__filename);
2139
3223
  const packageJsonPaths = [
2140
- (0, import_node_path8.join)(__dirname, "..", "..", "package.json"),
3224
+ (0, import_node_path13.join)(__dirname, "..", "..", "package.json"),
2141
3225
  // dist/server/index.js
2142
- (0, import_node_path8.join)(__dirname, "..", "package.json")
2143
- // dist/electron-server.cjs
3226
+ (0, import_node_path13.join)(__dirname, "..", "package.json")
3227
+ // bundled server entrypoint
2144
3228
  ];
2145
3229
  for (const packageJsonPath of packageJsonPaths) {
2146
- if (!(0, import_node_fs8.existsSync)(packageJsonPath)) continue;
2147
- const packageJson = JSON.parse((0, import_node_fs8.readFileSync)(packageJsonPath, "utf8"));
3230
+ if (!(0, import_node_fs13.existsSync)(packageJsonPath)) continue;
3231
+ const packageJson = JSON.parse((0, import_node_fs13.readFileSync)(packageJsonPath, "utf8"));
2148
3232
  if (packageJson.version) return packageJson.version;
2149
3233
  }
2150
3234
  return "unknown";
@@ -2243,18 +3327,19 @@ async function listenWithPortFallback(app, preferredPort) {
2243
3327
  throw new Error(`Could not find an available port starting from ${preferredPort}`);
2244
3328
  }
2245
3329
  function resolveStaticAssetBaseDir(moduleUrl = __esbuild_import_meta_url, baseDir) {
2246
- if (baseDir) return { baseDir: (0, import_node_path8.resolve)(baseDir), isProduction: true };
2247
- const moduleDir = (0, import_node_path8.dirname)((0, import_node_url.fileURLToPath)(moduleUrl));
3330
+ if (baseDir) return { baseDir: (0, import_node_path13.resolve)(baseDir), isProduction: true };
3331
+ const moduleDir = (0, import_node_path13.dirname)((0, import_node_url.fileURLToPath)(moduleUrl));
2248
3332
  const isProduction = moduleUrl.includes("/dist/");
2249
- if (!isProduction) return { baseDir: (0, import_node_path8.resolve)(moduleDir), isProduction: false };
2250
- if ((0, import_node_path8.basename)(moduleDir) === "server") {
2251
- return { baseDir: (0, import_node_path8.resolve)((0, import_node_path8.dirname)(moduleDir)), isProduction: true };
3333
+ if (!isProduction) return { baseDir: (0, import_node_path13.resolve)(moduleDir), isProduction: false };
3334
+ if ((0, import_node_path13.basename)(moduleDir) === "server") {
3335
+ return { baseDir: (0, import_node_path13.resolve)((0, import_node_path13.dirname)(moduleDir)), isProduction: true };
2252
3336
  }
2253
- return { baseDir: (0, import_node_path8.resolve)(moduleDir), isProduction: true };
3337
+ return { baseDir: (0, import_node_path13.resolve)(moduleDir), isProduction: true };
2254
3338
  }
2255
3339
  function createApp(_port, baseDir) {
2256
3340
  const app = (0, import_express.default)();
2257
3341
  const router = import_express.default.Router();
3342
+ app.use(import_express.default.json({ limit: "16kb" }));
2258
3343
  registerApiRoutes(router, {
2259
3344
  packageName: PACKAGE_NAME,
2260
3345
  version: getPackageVersion(),
@@ -2262,17 +3347,17 @@ function createApp(_port, baseDir) {
2262
3347
  });
2263
3348
  app.use("/api", router);
2264
3349
  const { baseDir: _baseDir, isProduction } = resolveStaticAssetBaseDir(__esbuild_import_meta_url, baseDir);
2265
- const popoverPath = isProduction ? (0, import_node_path8.join)(_baseDir, "client", "popover.html") : (0, import_node_path8.join)(_baseDir, "..", "..", "public", "popover.html");
3350
+ const popoverPath = isProduction ? (0, import_node_path13.join)(_baseDir, "client", "popover.html") : (0, import_node_path13.join)(_baseDir, "..", "..", "public", "popover.html");
2266
3351
  app.get("/popover.html", (_req, res, next) => {
2267
- if (!(0, import_node_fs8.existsSync)(popoverPath)) {
3352
+ if (!(0, import_node_fs13.existsSync)(popoverPath)) {
2268
3353
  next();
2269
3354
  return;
2270
3355
  }
2271
- res.type("html").send((0, import_node_fs8.readFileSync)(popoverPath, "utf8"));
3356
+ res.type("html").send((0, import_node_fs13.readFileSync)(popoverPath, "utf8"));
2272
3357
  });
2273
3358
  if (isProduction) {
2274
- const clientPath = (0, import_node_path8.join)(_baseDir, "client");
2275
- const clientIndexPath = (0, import_node_path8.join)(clientPath, "index.html");
3359
+ const clientPath = (0, import_node_path13.join)(_baseDir, "client");
3360
+ const clientIndexPath = (0, import_node_path13.join)(clientPath, "index.html");
2276
3361
  app.use(import_express.default.static(clientPath));
2277
3362
  app.use("{*path}", (_req, res) => {
2278
3363
  res.sendFile(clientIndexPath);
@@ -2294,13 +3379,21 @@ async function main() {
2294
3379
  process.exit(1);
2295
3380
  }
2296
3381
  console.log(`Starting tokendash v${version} in tray mode...`);
2297
- const { default: electronPath } = await import("electron");
2298
- const { spawn } = await import("node:child_process");
2299
- const child = spawn(electronPath, ["."], {
3382
+ const { spawn: spawn2 } = await import("node:child_process");
3383
+ const { resolve: resolve2 } = await import("node:path");
3384
+ const { existsSync: existsSync12 } = await import("node:fs");
3385
+ const moduleDir = (0, import_node_path13.dirname)((0, import_node_url.fileURLToPath)(__esbuild_import_meta_url));
3386
+ const packagedPath = resolve2(moduleDir, "..", "..", "TokenDashSwift", ".build", "debug", "TokenDash");
3387
+ const devPath = resolve2(moduleDir, "..", "..", "TokenDashSwift", ".build", "debug", "TokenDash");
3388
+ const swiftBin = existsSync12(packagedPath) ? packagedPath : devPath;
3389
+ if (!existsSync12(swiftBin)) {
3390
+ console.error('Error: TokenDash Swift binary not found. Run "npm run build:swift" first.');
3391
+ process.exit(1);
3392
+ }
3393
+ const child = spawn2(swiftBin, [], {
2300
3394
  env: {
2301
3395
  ...process.env,
2302
- TOKENDASH_PORT: String(preferredPort),
2303
- TOKENDASH_TRAY: "1"
3396
+ TOKENDASH_PORT: String(preferredPort)
2304
3397
  },
2305
3398
  stdio: "inherit"
2306
3399
  });
@@ -2329,11 +3422,14 @@ async function main() {
2329
3422
  console.log('Development mode - use "npm run dev" for full dev experience');
2330
3423
  }
2331
3424
  if (shouldOpenBrowser) {
2332
- setTimeout(() => {
3425
+ setTimeout(async () => {
2333
3426
  console.log("Opening dashboard in your browser...");
2334
- (0, import_open.default)(`http://localhost:${port}`).catch((err) => {
3427
+ try {
3428
+ const { default: open } = await import("open");
3429
+ await open(`http://localhost:${port}`);
3430
+ } catch (err) {
2335
3431
  console.warn("Could not open browser:", err.message);
2336
- });
3432
+ }
2337
3433
  }, 100);
2338
3434
  } else {
2339
3435
  console.log("Browser auto-open disabled (--no-open)");