@zhangferry-dev/tokendash 1.6.1 → 1.6.2

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 +146 -83
  2. package/dist/client/assets/index-Bw503sNp.css +1 -0
  3. package/dist/client/index.html +2 -2
  4. package/dist/daemon.cjs +3306 -0
  5. package/dist/daemon.cjs.map +7 -0
  6. package/dist/electron-server.cjs +1019 -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 +22 -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 +47 -0
  16. package/dist/server/quota/adapter.js +41 -0
  17. package/dist/server/quota/adapters/claude.d.ts +2 -0
  18. package/dist/server/quota/adapters/claude.js +124 -0
  19. package/dist/server/quota/adapters/codex.d.ts +2 -0
  20. package/dist/server/quota/adapters/codex.js +188 -0
  21. package/dist/server/quota/adapters/glm.d.ts +2 -0
  22. package/dist/server/quota/adapters/glm.js +133 -0
  23. package/dist/server/quota/adapters/kimi.d.ts +2 -0
  24. package/dist/server/quota/adapters/kimi.js +184 -0
  25. package/dist/server/quota/adapters/minimax.d.ts +2 -0
  26. package/dist/server/quota/adapters/minimax.js +77 -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 +37 -0
  36. package/dist/server/quota/quotaService.js +141 -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 +65 -0
  40. package/dist/server/quota/types.js +10 -0
  41. package/dist/server/routes/api.js +15 -0
  42. package/dist/server/routes/insights.d.ts +2 -0
  43. package/dist/server/routes/insights.js +155 -0
  44. package/package.json +6 -10
  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,987 @@ 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
+ async fetchWithTimeout(adapter) {
2263
+ try {
2264
+ const snapshot = await withTimeout(adapter.fetch(), this.fetchTimeoutMs, adapter.provider);
2265
+ const validated = validateQuotaSnapshot(snapshot);
2266
+ this.cache.set(validated);
2267
+ return validated;
2268
+ } catch (err) {
2269
+ return this.handleFailure(adapter, err);
2270
+ }
2271
+ }
2272
+ handleFailure(adapter, err) {
2273
+ let status;
2274
+ if (err instanceof QuotaError) {
2275
+ status = err.status;
2276
+ } else if (err instanceof TimeoutError) {
2277
+ status = { state: "timed_out", message: `upstream did not respond within ${this.fetchTimeoutMs}ms` };
2278
+ } else {
2279
+ status = { state: "error", message: redact(err), category: "unexpected" };
2280
+ }
2281
+ const stale = this.cache.getStale(adapter.provider);
2282
+ if (stale) {
2283
+ return { ...stale, freshness: "stale", status };
2284
+ }
2285
+ return {
2286
+ provider: adapter.provider,
2287
+ displayName: adapter.displayName,
2288
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2289
+ freshness: "stale",
2290
+ windows: [],
2291
+ status
2292
+ };
2293
+ }
2294
+ };
2295
+ var TimeoutError = class extends Error {
2296
+ constructor(provider) {
2297
+ super(`quota fetch timed out: ${provider}`);
2298
+ this.name = "TimeoutError";
2299
+ }
2300
+ };
2301
+ function withTimeout(p, ms, provider) {
2302
+ return new Promise((resolve2, reject) => {
2303
+ const timer = setTimeout(() => reject(new TimeoutError(provider)), ms);
2304
+ p.then(
2305
+ (v) => {
2306
+ clearTimeout(timer);
2307
+ resolve2(v);
2308
+ },
2309
+ (e) => {
2310
+ clearTimeout(timer);
2311
+ reject(e);
2312
+ }
2313
+ );
2314
+ });
2315
+ }
2316
+ async function safeIsConfigured(adapter) {
2317
+ try {
2318
+ return await adapter.isConfigured();
2319
+ } catch {
2320
+ return false;
2321
+ }
2322
+ }
2323
+ function redact(err) {
2324
+ const msg = err instanceof Error ? err.message : String(err);
2325
+ 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);
2326
+ }
2327
+
2328
+ // src/server/quota/adapters/codex.ts
2329
+ var import_node_fs8 = require("node:fs");
2330
+ var import_node_path8 = require("node:path");
2331
+ var import_node_os8 = require("node:os");
2332
+ var import_node_child_process2 = require("node:child_process");
2333
+
2334
+ // src/server/quota/helpers.ts
2335
+ function unixToIso(unix) {
2336
+ if (unix === null || unix === void 0 || unix === "") return void 0;
2337
+ const n = typeof unix === "string" ? parseInt(unix, 10) : unix;
2338
+ if (!Number.isFinite(n) || n <= 0) return void 0;
2339
+ const ms = n > 1e12 ? n : n * 1e3;
2340
+ return new Date(ms).toISOString();
2341
+ }
2342
+ function clampPercent(v) {
2343
+ if (!Number.isFinite(v)) return 0;
2344
+ return Math.max(0, Math.min(100, v));
2345
+ }
2346
+ function round1(v) {
2347
+ return Math.round(v * 10) / 10;
2348
+ }
2349
+ function windowFromPercent(id, label, usedPercent, opts = {}) {
2350
+ const used = round1(clampPercent(usedPercent));
2351
+ return {
2352
+ id,
2353
+ label,
2354
+ usedPercent: used,
2355
+ remainingPercent: round1(100 - used),
2356
+ durationMins: opts.durationMins,
2357
+ resetsAt: opts.resetsAt,
2358
+ used: opts.used,
2359
+ limit: opts.limit,
2360
+ modelName: opts.modelName,
2361
+ isUnlimited: opts.isUnlimited
2362
+ };
2363
+ }
2364
+ function windowFromCounts(id, label, used, limit, opts = {}) {
2365
+ if (limit <= 0) {
2366
+ return { id, label, usedPercent: 0, remainingPercent: 100, used, limit, isUnlimited: true, ...opts };
2367
+ }
2368
+ const pct = round1(clampPercent(used / limit * 100));
2369
+ return {
2370
+ id,
2371
+ label,
2372
+ usedPercent: pct,
2373
+ remainingPercent: round1(100 - pct),
2374
+ used,
2375
+ limit,
2376
+ ...opts
2377
+ };
2378
+ }
2379
+ async function fetchJsonWithTimeout(url, opts = {}) {
2380
+ const ctrl = new AbortController();
2381
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 8e3);
2382
+ try {
2383
+ const res = await fetch(url, { headers: opts.headers, signal: ctrl.signal });
2384
+ if (!res.ok) {
2385
+ const body = await res.text().catch(() => "");
2386
+ throw new HttpError(res.status, body.slice(0, 200));
2387
+ }
2388
+ return await res.json();
2389
+ } finally {
2390
+ clearTimeout(timer);
2391
+ }
2392
+ }
2393
+ var HttpError = class extends Error {
2394
+ constructor(status, body) {
2395
+ super(`HTTP ${status}`);
2396
+ this.status = status;
2397
+ this.body = body;
2398
+ this.name = "HttpError";
2399
+ }
2400
+ };
2401
+ function classifyHttpError(err) {
2402
+ if (err.status === 401 || err.status === 403) {
2403
+ return { state: "auth_failed", message: "credential rejected by provider" };
2404
+ }
2405
+ if (err.status === 429) {
2406
+ return { state: "rate_limited", message: "provider throttled the request" };
2407
+ }
2408
+ return { state: "upstream_unavailable", message: `provider returned HTTP ${err.status}` };
2409
+ }
2410
+
2411
+ // src/server/quota/adapters/codex.ts
2412
+ function codexHome() {
2413
+ return process.env.CODEX_HOME || (0, import_node_path8.join)((0, import_node_os8.homedir)(), ".codex");
2414
+ }
2415
+ var codexAdapter = {
2416
+ provider: "codex",
2417
+ displayName: "OpenAI Codex",
2418
+ async isConfigured() {
2419
+ if ((0, import_node_fs8.existsSync)((0, import_node_path8.join)(codexHome(), "auth.json"))) return true;
2420
+ return await codexBinaryAvailable();
2421
+ },
2422
+ async fetch() {
2423
+ const result = await queryRateLimits();
2424
+ const buckets = result.rateLimitsByLimitId ?? (result.rateLimits ? { primary: result.rateLimits } : {});
2425
+ const windows = [];
2426
+ for (const [key, bucket] of Object.entries(buckets)) {
2427
+ if (bucket.primary) {
2428
+ windows.push(windowFromPercent(`codex_${key}_primary`, labelForBucket(key, "primary", bucket), bucket.primary.usedPercent ?? 0, {
2429
+ durationMins: bucket.primary.windowDurationMins,
2430
+ resetsAt: unixToIso(bucket.primary.resetsAt)
2431
+ }));
2432
+ }
2433
+ if (bucket.secondary) {
2434
+ windows.push(windowFromPercent(`codex_${key}_secondary`, labelForBucket(key, "secondary", bucket), bucket.secondary.usedPercent ?? 0, {
2435
+ durationMins: bucket.secondary.windowDurationMins,
2436
+ resetsAt: unixToIso(bucket.secondary.resetsAt)
2437
+ }));
2438
+ }
2439
+ }
2440
+ const snap = baseSnapshot("codex", "OpenAI Codex", {
2441
+ planName: result.planType ? capitalize(result.planType) : void 0,
2442
+ windows
2443
+ });
2444
+ return { ...snap, status: { state: "ok" } };
2445
+ }
2446
+ };
2447
+ function labelForBucket(key, tier, bucket) {
2448
+ const dur = tier === "primary" ? bucket.primary?.windowDurationMins : bucket.secondary?.windowDurationMins;
2449
+ const durLabel = dur ? durationLabel(dur) : capitalize(tier);
2450
+ const who = bucket.limitName || (key && key !== "primary" ? key : "");
2451
+ return who ? `${capitalize(who)} \xB7 ${durLabel}` : durLabel;
2452
+ }
2453
+ function durationLabel(mins) {
2454
+ if (mins >= 10080) return "Weekly";
2455
+ if (mins >= 1440) return `${Math.round(mins / 1440)}-Day`;
2456
+ if (mins >= 60) return `${Math.round(mins / 60)}-Hour`;
2457
+ return `${mins}m`;
2458
+ }
2459
+ function capitalize(s) {
2460
+ return s.charAt(0).toUpperCase() + s.slice(1);
2461
+ }
2462
+ async function queryRateLimits() {
2463
+ if (!await codexBinaryAvailable()) {
2464
+ throw new QuotaError({ state: "not_configured", message: "codex CLI not found on PATH" });
2465
+ }
2466
+ const proc = (0, import_node_child_process2.spawn)("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
2467
+ const client = new JsonRpcClient(proc);
2468
+ try {
2469
+ await client.request("initialize", {
2470
+ protocolVersion: "2025-03-26",
2471
+ clientInfo: { name: "tokendash", version: "1.0.0" }
2472
+ });
2473
+ client.notify("initialized", {});
2474
+ const res = await client.request("account/rateLimits/read", {});
2475
+ return res;
2476
+ } catch (err) {
2477
+ throw toQuotaError(err);
2478
+ } finally {
2479
+ client.dispose();
2480
+ try {
2481
+ proc.kill("SIGKILL");
2482
+ } catch {
2483
+ }
2484
+ }
2485
+ }
2486
+ function toQuotaError(err) {
2487
+ const msg = err instanceof Error ? err.message : String(err);
2488
+ if (/not found|ENOENT|spawn/i.test(msg)) {
2489
+ return new QuotaError({ state: "not_configured", message: "codex app-server unavailable" });
2490
+ }
2491
+ if (/401|403|unauthor|auth/i.test(msg)) {
2492
+ return new QuotaError({ state: "auth_failed", message: "codex session not authenticated" });
2493
+ }
2494
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2495
+ }
2496
+ var JsonRpcClient = class {
2497
+ constructor(proc) {
2498
+ this.proc = proc;
2499
+ proc.stdout.setEncoding("utf8");
2500
+ proc.stdout.on("data", (chunk) => this.onData(chunk));
2501
+ proc.on("error", (err) => this.failAll(err));
2502
+ proc.on("close", () => this.failAll(new Error("codex app-server closed unexpectedly")));
2503
+ }
2504
+ id = 0;
2505
+ buffer = "";
2506
+ pending = /* @__PURE__ */ new Map();
2507
+ disposed = false;
2508
+ request(method, params) {
2509
+ if (this.disposed) return Promise.reject(new Error("client disposed"));
2510
+ const id = ++this.id;
2511
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
2512
+ return new Promise((resolve2, reject) => {
2513
+ this.pending.set(id, { resolve: resolve2, reject });
2514
+ this.proc.stdin.write(msg, (err) => {
2515
+ if (err) reject(err instanceof Error ? err : new Error(String(err)));
2516
+ });
2517
+ });
2518
+ }
2519
+ notify(method, params) {
2520
+ if (this.disposed) return;
2521
+ const msg = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
2522
+ this.proc.stdin.write(msg, () => {
2523
+ });
2524
+ }
2525
+ dispose() {
2526
+ this.disposed = true;
2527
+ this.failAll(new Error("disposed"));
2528
+ }
2529
+ onData(chunk) {
2530
+ this.buffer += chunk;
2531
+ let idx;
2532
+ while ((idx = this.buffer.indexOf("\n")) >= 0) {
2533
+ const line = this.buffer.slice(0, idx).trim();
2534
+ this.buffer = this.buffer.slice(idx + 1);
2535
+ if (!line) continue;
2536
+ let msg;
2537
+ try {
2538
+ msg = JSON.parse(line);
2539
+ } catch {
2540
+ continue;
2541
+ }
2542
+ if (msg.id === void 0) continue;
2543
+ const entry = this.pending.get(msg.id);
2544
+ if (!entry) continue;
2545
+ this.pending.delete(msg.id);
2546
+ if (msg.error) entry.reject(new Error(msg.error.message || "codex JSON-RPC error"));
2547
+ else entry.resolve(msg.result);
2548
+ }
2549
+ }
2550
+ failAll(err) {
2551
+ for (const [, entry] of this.pending) entry.reject(err);
2552
+ this.pending.clear();
2553
+ }
2554
+ };
2555
+ var cachedCodexPath;
2556
+ async function codexBinaryAvailable() {
2557
+ if (cachedCodexPath !== void 0) return cachedCodexPath !== null;
2558
+ return new Promise((resolve2) => {
2559
+ const proc = (0, import_node_child_process2.spawn)("which", ["codex"], { stdio: ["ignore", "pipe", "ignore"] });
2560
+ let out = "";
2561
+ proc.stdout.on("data", (c) => {
2562
+ out += c;
2563
+ });
2564
+ proc.on("close", () => {
2565
+ cachedCodexPath = out.trim() || null;
2566
+ resolve2(cachedCodexPath !== null);
2567
+ });
2568
+ proc.on("error", () => {
2569
+ cachedCodexPath = null;
2570
+ resolve2(false);
2571
+ });
2572
+ });
2573
+ }
2574
+
2575
+ // src/server/quota/adapters/claude.ts
2576
+ var import_node_fs9 = require("node:fs");
2577
+ var import_node_path9 = require("node:path");
2578
+ var import_node_os9 = require("node:os");
2579
+ var import_node_child_process3 = require("node:child_process");
2580
+ var claudeAdapter = {
2581
+ provider: "claude",
2582
+ displayName: "Claude Code",
2583
+ async isConfigured() {
2584
+ const token = readClaudeToken();
2585
+ return !!token;
2586
+ },
2587
+ async fetch() {
2588
+ const token = readClaudeToken();
2589
+ if (!token) {
2590
+ throw new QuotaError({ state: "not_configured", message: "no Claude Code OAuth credential found" });
2591
+ }
2592
+ let data;
2593
+ try {
2594
+ data = await fetchJsonWithTimeout("https://api.anthropic.com/api/oauth/usage", {
2595
+ headers: {
2596
+ Authorization: `Bearer ${token}`,
2597
+ "anthropic-beta": "oauth-2025-04-20",
2598
+ "Content-Type": "application/json"
2599
+ }
2600
+ });
2601
+ } catch (err) {
2602
+ throw classifyFetchError(err);
2603
+ }
2604
+ const windows = [];
2605
+ if (data.five_hour) {
2606
+ windows.push(windowFromPercent("five_hour", "5-Hour Window", data.five_hour.utilization ?? 0, {
2607
+ durationMins: 300,
2608
+ resetsAt: normalizeIso(data.five_hour.resets_at)
2609
+ }));
2610
+ }
2611
+ if (data.seven_day) {
2612
+ windows.push(windowFromPercent("seven_day", "Weekly", data.seven_day.utilization ?? 0, {
2613
+ durationMins: 10080,
2614
+ resetsAt: normalizeIso(data.seven_day.resets_at)
2615
+ }));
2616
+ }
2617
+ if (data.seven_day_opus?.utilization !== void 0 && data.seven_day_opus?.utilization !== null) {
2618
+ windows.push(windowFromPercent("seven_day_opus", "Weekly \xB7 Opus", data.seven_day_opus.utilization, {
2619
+ durationMins: 10080,
2620
+ resetsAt: normalizeIso(data.seven_day_opus.resets_at)
2621
+ }));
2622
+ }
2623
+ const snap = baseSnapshot("claude", "Claude Code", { windows });
2624
+ return { ...snap, status: { state: "ok" } };
2625
+ }
2626
+ };
2627
+ function classifyFetchError(err) {
2628
+ if (err instanceof HttpError) {
2629
+ const c = classifyHttpError(err);
2630
+ return new QuotaError(c);
2631
+ }
2632
+ const msg = err instanceof Error ? err.message : String(err);
2633
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2634
+ }
2635
+ function normalizeIso(s) {
2636
+ return s ? new Date(s).toISOString() : void 0;
2637
+ }
2638
+ function readClaudeToken() {
2639
+ if (process.platform === "darwin") {
2640
+ const token = readFromKeychain();
2641
+ if (token) return token;
2642
+ }
2643
+ const configDir = process.env.CLAUDE_CONFIG_DIR || (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".claude");
2644
+ const credPath = (0, import_node_path9.join)(configDir, ".credentials.json");
2645
+ if ((0, import_node_fs9.existsSync)(credPath)) {
2646
+ try {
2647
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)(credPath, "utf8"));
2648
+ return parsed?.claudeAiOauth?.accessToken ?? null;
2649
+ } catch {
2650
+ return null;
2651
+ }
2652
+ }
2653
+ return null;
2654
+ }
2655
+ function readFromKeychain() {
2656
+ const candidates = ["Claude Code-credentials"];
2657
+ try {
2658
+ const list = (0, import_node_child_process3.execFileSync)("security", ["dump-keychain"], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
2659
+ for (const m of list.matchAll(/"srvname"<blob>="([^"]*Claude Code-credentials[^"]*)"/g)) {
2660
+ if (m[1] && !candidates.includes(m[1])) candidates.push(m[1]);
2661
+ }
2662
+ } catch {
2663
+ }
2664
+ for (const name of candidates) {
2665
+ try {
2666
+ const raw = (0, import_node_child_process3.execFileSync)("security", ["find-generic-password", "-s", name, "-w"], {
2667
+ stdio: ["ignore", "pipe", "ignore"],
2668
+ encoding: "utf8"
2669
+ }).trim();
2670
+ if (!raw) continue;
2671
+ try {
2672
+ const parsed = JSON.parse(raw);
2673
+ return parsed?.claudeAiOauth?.accessToken ?? null;
2674
+ } catch {
2675
+ return raw;
2676
+ }
2677
+ } catch {
2678
+ continue;
2679
+ }
2680
+ }
2681
+ return null;
2682
+ }
2683
+
2684
+ // src/server/quota/adapters/glm.ts
2685
+ var import_node_fs11 = require("node:fs");
2686
+ var import_node_path11 = require("node:path");
2687
+ var import_node_os11 = require("node:os");
2688
+
2689
+ // src/server/quota/credentialsFile.ts
2690
+ var import_node_fs10 = require("node:fs");
2691
+ var import_node_path10 = require("node:path");
2692
+ var import_node_os10 = require("node:os");
2693
+ function readStoredCredential(provider) {
2694
+ try {
2695
+ const path = (0, import_node_path10.join)((0, import_node_os10.homedir)(), ".tokendash", "credentials.json");
2696
+ if (!(0, import_node_fs10.existsSync)(path)) return null;
2697
+ const all = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
2698
+ const entry = all?.[provider];
2699
+ if (entry && typeof entry === "object" && typeof entry.apiKey === "string") {
2700
+ const apiKey = entry.apiKey;
2701
+ if (!apiKey) return null;
2702
+ const baseUrl = entry.baseUrl;
2703
+ return { apiKey, baseUrl: typeof baseUrl === "string" ? baseUrl : void 0 };
2704
+ }
2705
+ return null;
2706
+ } catch {
2707
+ return null;
2708
+ }
2709
+ }
2710
+
2711
+ // src/server/quota/adapters/glm.ts
2712
+ var glmAdapter = {
2713
+ provider: "glm",
2714
+ displayName: "GLM Coding Plan",
2715
+ async isConfigured() {
2716
+ return !!resolveCredential();
2717
+ },
2718
+ async fetch() {
2719
+ const cred = resolveCredential();
2720
+ if (!cred) {
2721
+ throw new QuotaError({ state: "not_configured", message: "set ZAI_API_KEY or ZHIPU_API_KEY" });
2722
+ }
2723
+ let data;
2724
+ try {
2725
+ data = await fetchJsonWithTimeout(`${cred.base}/api/monitor/usage/quota/limit`, {
2726
+ headers: {
2727
+ // GLM wants the raw key, NOT "Bearer <key>".
2728
+ Authorization: cred.key,
2729
+ "Accept-Language": "en-US,en",
2730
+ "Content-Type": "application/json"
2731
+ }
2732
+ });
2733
+ } catch (err) {
2734
+ throw classifyFetchError2(err);
2735
+ }
2736
+ if (!data?.success && data?.code !== 200) {
2737
+ throw new QuotaError({ state: "upstream_unavailable", message: data?.msg || "GLM quota request failed" });
2738
+ }
2739
+ const limits = data.data?.limits ?? [];
2740
+ const windows = [];
2741
+ const tokenLimits = limits.filter((l) => l.type === "TOKENS_LIMIT" && typeof l.percentage === "number").sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
2742
+ tokenLimits.forEach((l, i) => {
2743
+ const isShort = i === 0;
2744
+ windows.push(windowFromPercent(
2745
+ isShort ? "glm_5h" : "glm_weekly",
2746
+ isShort ? "5-Hour Window" : "Weekly",
2747
+ l.percentage ?? 0,
2748
+ { durationMins: isShort ? 300 : 10080, resetsAt: unixToIso(l.nextResetTime) }
2749
+ ));
2750
+ });
2751
+ const timeLimit = limits.find((l) => l.type === "TIME_LIMIT");
2752
+ if (timeLimit && typeof timeLimit.usage === "number") {
2753
+ windows.push(windowFromCounts(
2754
+ "glm_mcp_monthly",
2755
+ "MCP \xB7 Monthly",
2756
+ timeLimit.currentValue ?? 0,
2757
+ timeLimit.usage
2758
+ ));
2759
+ }
2760
+ const snap = baseSnapshot("glm", "GLM Coding Plan", {
2761
+ planName: data.data?.level ? capitalize2(data.data.level) : void 0,
2762
+ windows
2763
+ });
2764
+ return { ...snap, status: { state: "ok" } };
2765
+ }
2766
+ };
2767
+ function classifyFetchError2(err) {
2768
+ if (err instanceof HttpError) {
2769
+ const c = classifyHttpError(err);
2770
+ return new QuotaError(c);
2771
+ }
2772
+ const msg = err instanceof Error ? err.message : String(err);
2773
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2774
+ }
2775
+ function resolveCredential() {
2776
+ const stored = readStoredCredential("glm");
2777
+ if (stored) return { key: stored.apiKey, base: stored.baseUrl || "https://open.bigmodel.cn" };
2778
+ const zai = envOrConfig("ZAI_API_KEY");
2779
+ if (zai) return { key: zai, base: envOrConfig("ZAI_BASE_URL") || "https://api.z.ai" };
2780
+ const zhipu = envOrConfig("ZHIPU_API_KEY");
2781
+ if (zhipu) return { key: zhipu, base: envOrConfig("ZHIPU_BASE_URL") || "https://open.bigmodel.cn" };
2782
+ const anthropicBase = envOrConfig("ANTHROPIC_BASE_URL");
2783
+ if (anthropicBase && isGlmHost(anthropicBase)) {
2784
+ const origin = originOf(anthropicBase);
2785
+ const token = envOrConfig("ANTHROPIC_AUTH_TOKEN") || envOrConfig("ANTHROPIC_API_KEY");
2786
+ if (origin && token) return { key: token, base: origin };
2787
+ }
2788
+ return null;
2789
+ }
2790
+ function isGlmHost(url) {
2791
+ const h = url.toLowerCase();
2792
+ return h.includes("bigmodel.cn") || h.includes("z.ai");
2793
+ }
2794
+ function originOf(url) {
2795
+ try {
2796
+ const u = new URL(url);
2797
+ return `${u.protocol}//${u.host}`;
2798
+ } catch {
2799
+ return null;
2800
+ }
2801
+ }
2802
+ var claudeSettingsEnv;
2803
+ function envOrConfig(key) {
2804
+ if (process.env[key]) return process.env[key];
2805
+ if (claudeSettingsEnv === void 0) claudeSettingsEnv = loadClaudeSettingsEnv();
2806
+ return claudeSettingsEnv?.[key];
2807
+ }
2808
+ function loadClaudeSettingsEnv() {
2809
+ const configDir = process.env.CLAUDE_CONFIG_DIR || (0, import_node_path11.join)((0, import_node_os11.homedir)(), ".claude");
2810
+ try {
2811
+ const path = (0, import_node_path11.join)(configDir, "settings.json");
2812
+ if (!(0, import_node_fs11.existsSync)(path)) return null;
2813
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
2814
+ return parsed?.env && typeof parsed.env === "object" ? parsed.env : null;
2815
+ } catch {
2816
+ return null;
2817
+ }
2818
+ }
2819
+ function capitalize2(s) {
2820
+ return s.charAt(0).toUpperCase() + s.slice(1);
2821
+ }
2822
+
2823
+ // src/server/quota/adapters/minimax.ts
2824
+ var minimaxAdapter = {
2825
+ provider: "minimax",
2826
+ displayName: "MiniMax Coding Plan",
2827
+ async isConfigured() {
2828
+ return !!resolveCredential2();
2829
+ },
2830
+ async fetch() {
2831
+ const cred = resolveCredential2();
2832
+ if (!cred) {
2833
+ throw new QuotaError({ state: "not_configured", message: "set MINIMAX_API_KEY (Subscription Key)" });
2834
+ }
2835
+ let data;
2836
+ try {
2837
+ data = await fetchJsonWithTimeout(`${cred.base}/v1/token_plan/remains`, {
2838
+ headers: {
2839
+ Authorization: `Bearer ${cred.key}`,
2840
+ "Content-Type": "application/json"
2841
+ }
2842
+ });
2843
+ } catch (err) {
2844
+ throw classifyFetchError3(err);
2845
+ }
2846
+ if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
2847
+ throw new QuotaError({
2848
+ state: "upstream_unavailable",
2849
+ message: data.base_resp.status_msg || `MiniMax error ${data.base_resp.status_code}`
2850
+ });
2851
+ }
2852
+ const windows = [];
2853
+ for (const m of data.model_remains ?? []) {
2854
+ const model = m.model_name ?? "MiniMax";
2855
+ const intervalTotal = m.current_interval_total_count ?? 0;
2856
+ const intervalRemaining = m.current_interval_usage_count ?? 0;
2857
+ if (intervalTotal > 0) {
2858
+ windows.push(windowFromCounts(
2859
+ `minimax_5h_${model}`,
2860
+ `5-Hour \xB7 ${model}`,
2861
+ Math.max(0, intervalTotal - intervalRemaining),
2862
+ intervalTotal,
2863
+ { durationMins: 300, resetsAt: unixToIso(m.end_time) }
2864
+ ));
2865
+ }
2866
+ const weeklyTotal = m.current_weekly_total_count ?? 0;
2867
+ const weeklyRemaining = m.current_weekly_usage_count ?? 0;
2868
+ if (weeklyTotal > 0) {
2869
+ windows.push(windowFromCounts(
2870
+ `minimax_weekly_${model}`,
2871
+ `Weekly \xB7 ${model}`,
2872
+ Math.max(0, weeklyTotal - weeklyRemaining),
2873
+ weeklyTotal,
2874
+ { durationMins: 10080 }
2875
+ ));
2876
+ }
2877
+ }
2878
+ const snap = baseSnapshot("minimax", "MiniMax Coding Plan", { windows });
2879
+ return { ...snap, status: { state: "ok" } };
2880
+ }
2881
+ };
2882
+ function classifyFetchError3(err) {
2883
+ if (err instanceof HttpError) {
2884
+ const c = classifyHttpError(err);
2885
+ return new QuotaError(c);
2886
+ }
2887
+ const msg = err instanceof Error ? err.message : String(err);
2888
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2889
+ }
2890
+ function resolveCredential2() {
2891
+ const stored = readStoredCredential("minimax");
2892
+ if (stored) {
2893
+ const region2 = (process.env.MINIMAX_REGION || "").toLowerCase();
2894
+ const base2 = stored.baseUrl || (region2 === "cn" ? "https://www.minimaxi.com" : "https://www.minimax.io");
2895
+ return { key: stored.apiKey, base: base2 };
2896
+ }
2897
+ const key = process.env.MINIMAX_API_KEY || process.env.MINIMAX_SUBSCRIPTION_KEY;
2898
+ if (!key) return null;
2899
+ const region = (process.env.MINIMAX_REGION || "").toLowerCase();
2900
+ const base = region === "cn" ? process.env.MINIMAX_BASE_URL || "https://www.minimaxi.com" : process.env.MINIMAX_BASE_URL || "https://www.minimax.io";
2901
+ return { key, base };
2902
+ }
2903
+
2904
+ // src/server/quota/adapters/kimi.ts
2905
+ var import_node_fs12 = require("node:fs");
2906
+ var import_node_path12 = require("node:path");
2907
+ var import_node_os12 = require("node:os");
2908
+ var KIMI_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
2909
+ var KIMI_BASE = "https://api.kimi.com/coding/v1";
2910
+ var KIMI_AUTH = "https://auth.kimi.com/api/oauth/token";
2911
+ var kimiAdapter = {
2912
+ provider: "kimi",
2913
+ displayName: "Kimi Code",
2914
+ async isConfigured() {
2915
+ const cred = readCredentials();
2916
+ return !!cred && !!cred.access_token;
2917
+ },
2918
+ async fetch() {
2919
+ const credPath = credentialsPath();
2920
+ let cred = readCredentials();
2921
+ if (!cred || !cred.access_token) {
2922
+ throw new QuotaError({ state: "not_configured", message: "run `kimi` to log in first" });
2923
+ }
2924
+ const nowSec = Math.floor(Date.now() / 1e3);
2925
+ if (cred.expires_at && cred.expires_at - nowSec < 300 && cred.refresh_token) {
2926
+ cred = await refreshToken(credPath, cred.refresh_token).catch(() => cred);
2927
+ }
2928
+ let data;
2929
+ try {
2930
+ data = await fetchJsonWithTimeout(`${KIMI_BASE}/usages`, {
2931
+ headers: {
2932
+ Authorization: `Bearer ${cred.access_token}`,
2933
+ Accept: "application/json"
2934
+ }
2935
+ });
2936
+ } catch (err) {
2937
+ throw classifyFetchError4(err);
2938
+ }
2939
+ const windows = [];
2940
+ if (data.usage) {
2941
+ const { used, limit } = toCounts(data.usage);
2942
+ if (limit > 0) {
2943
+ windows.push(windowFromCounts("kimi_weekly", "Weekly", used, limit, {
2944
+ durationMins: 10080,
2945
+ resetsAt: normalizeIso2(data.usage.resetTime)
2946
+ }));
2947
+ }
2948
+ }
2949
+ for (let i = 0; i < (data.limits?.length ?? 0); i++) {
2950
+ const entry = data.limits[i];
2951
+ const detail = entry?.detail;
2952
+ if (!detail) continue;
2953
+ const { used, limit } = toCounts(detail);
2954
+ if (limit <= 0) continue;
2955
+ const mins = minutesForWindow(entry?.window);
2956
+ windows.push(windowFromCounts(`kimi_limit_${i}`, mins ? `${durationLabel2(mins)} Window` : `Window ${i + 1}`, used, limit, {
2957
+ durationMins: mins,
2958
+ resetsAt: normalizeIso2(detail.resetTime)
2959
+ }));
2960
+ }
2961
+ const snap = baseSnapshot("kimi", "Kimi Code", {
2962
+ planName: data.user?.membership?.level ? prettifyLevel(data.user.membership.level) : void 0,
2963
+ windows
2964
+ });
2965
+ return { ...snap, status: { state: "ok" } };
2966
+ }
2967
+ };
2968
+ function classifyFetchError4(err) {
2969
+ if (err instanceof HttpError) {
2970
+ const c = classifyHttpError(err);
2971
+ return new QuotaError(c);
2972
+ }
2973
+ const msg = err instanceof Error ? err.message : String(err);
2974
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2975
+ }
2976
+ function toCounts(detail) {
2977
+ const limit = toNumber(detail.limit);
2978
+ const remaining = toNumber(detail.remaining);
2979
+ if (limit <= 0) return { used: 0, limit: 0 };
2980
+ return { used: Math.max(0, limit - remaining), limit };
2981
+ }
2982
+ function toNumber(v) {
2983
+ if (v === void 0 || v === null) return 0;
2984
+ const n = typeof v === "string" ? parseFloat(v) : v;
2985
+ return Number.isFinite(n) ? n : 0;
2986
+ }
2987
+ function minutesForWindow(window) {
2988
+ if (!window?.duration) return void 0;
2989
+ const unit = (window.timeUnit || "").toUpperCase();
2990
+ if (unit.includes("HOUR")) return window.duration * 60;
2991
+ if (unit.includes("DAY")) return window.duration * 1440;
2992
+ return window.duration;
2993
+ }
2994
+ function durationLabel2(mins) {
2995
+ if (mins >= 10080) return "Weekly";
2996
+ if (mins >= 1440) return `${Math.round(mins / 1440)}-Day`;
2997
+ if (mins >= 60) return `${Math.round(mins / 60)}-Hour`;
2998
+ return `${mins}m`;
2999
+ }
3000
+ function normalizeIso2(s) {
3001
+ return s ? new Date(s).toISOString() : void 0;
3002
+ }
3003
+ function prettifyLevel(level) {
3004
+ return level.replace(/^LEVEL_/, "").toLowerCase().replace(/(^|_)(\w)/g, (_, __, c) => " " + c.toUpperCase()).trim();
3005
+ }
3006
+ function kimiDataDir() {
3007
+ return process.env.KIMI_DATA_DIR || (0, import_node_path12.join)((0, import_node_os12.homedir)(), ".kimi");
3008
+ }
3009
+ function credentialsPath() {
3010
+ return (0, import_node_path12.join)(kimiDataDir(), "credentials", "kimi-code.json");
3011
+ }
3012
+ function readCredentials() {
3013
+ const stored = readStoredCredential("kimi");
3014
+ if (stored?.apiKey) {
3015
+ return { access_token: stored.apiKey, token_type: "Bearer" };
3016
+ }
3017
+ const path = credentialsPath();
3018
+ if (!(0, import_node_fs12.existsSync)(path)) return null;
3019
+ try {
3020
+ return JSON.parse((0, import_node_fs12.readFileSync)(path, "utf8"));
3021
+ } catch {
3022
+ return null;
3023
+ }
3024
+ }
3025
+ async function refreshToken(credPath, refreshToken2) {
3026
+ const body = new URLSearchParams({
3027
+ client_id: KIMI_CLIENT_ID,
3028
+ grant_type: "refresh_token",
3029
+ refresh_token: refreshToken2
3030
+ });
3031
+ const res = await fetch(KIMI_AUTH, {
3032
+ method: "POST",
3033
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3034
+ body
3035
+ });
3036
+ if (!res.ok) throw new Error(`token refresh failed: HTTP ${res.status}`);
3037
+ const tokens = await res.json();
3038
+ const updated = {
3039
+ access_token: tokens.access_token,
3040
+ refresh_token: tokens.refresh_token || refreshToken2,
3041
+ expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 3600),
3042
+ token_type: tokens.token_type || "Bearer"
3043
+ };
3044
+ try {
3045
+ (0, import_node_fs12.writeFileSync)(credPath, JSON.stringify(updated, null, 2), "utf8");
3046
+ } catch {
3047
+ }
3048
+ return updated;
3049
+ }
3050
+
3051
+ // src/server/quota/index.ts
3052
+ var registry = new QuotaAdapterRegistry();
3053
+ registry.register(claudeAdapter);
3054
+ registry.register(codexAdapter);
3055
+ registry.register(glmAdapter);
3056
+ registry.register(minimaxAdapter);
3057
+ registry.register(kimiAdapter);
3058
+ var quotaCache = new QuotaCache();
3059
+ var quotaService = new QuotaService(registry, quotaCache);
3060
+
2091
3061
  // src/server/routes/api.ts
3062
+ async function getQuota(_req, res) {
3063
+ const force = _req.query.refresh === "1" || _req.query.refresh === "true";
3064
+ try {
3065
+ const data = force ? await quotaService.refreshAll() : await quotaService.fetchAll();
3066
+ res.json(data);
3067
+ } catch (error) {
3068
+ const message = error instanceof Error ? error.message : "Unknown error";
3069
+ res.status(500).json({ error: "Failed to fetch quota", hint: message });
3070
+ }
3071
+ }
2092
3072
  function getAgents(_req, res) {
2093
3073
  try {
2094
3074
  const agents = detectAvailableAgents();
@@ -2121,10 +3101,10 @@ function registerApiRoutes(router, appInfo) {
2121
3101
  router.get("/projects", getProjects);
2122
3102
  router.get("/blocks", getBlocks);
2123
3103
  router.get("/analytics", getAnalytics);
3104
+ router.get("/quota", getQuota);
2124
3105
  }
2125
3106
 
2126
3107
  // src/server/index.ts
2127
- var import_open = __toESM(require("open"), 1);
2128
3108
  var CLI_USAGE = [
2129
3109
  "Usage:",
2130
3110
  " tokendash",
@@ -2135,16 +3115,16 @@ var CLI_USAGE = [
2135
3115
  var PACKAGE_NAME = "@zhangferry-dev/tokendash";
2136
3116
  function getPackageVersion() {
2137
3117
  const __filename = (0, import_node_url.fileURLToPath)(__esbuild_import_meta_url);
2138
- const __dirname = (0, import_node_path8.dirname)(__filename);
3118
+ const __dirname = (0, import_node_path13.dirname)(__filename);
2139
3119
  const packageJsonPaths = [
2140
- (0, import_node_path8.join)(__dirname, "..", "..", "package.json"),
3120
+ (0, import_node_path13.join)(__dirname, "..", "..", "package.json"),
2141
3121
  // dist/server/index.js
2142
- (0, import_node_path8.join)(__dirname, "..", "package.json")
2143
- // dist/electron-server.cjs
3122
+ (0, import_node_path13.join)(__dirname, "..", "package.json")
3123
+ // bundled server entrypoint
2144
3124
  ];
2145
3125
  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"));
3126
+ if (!(0, import_node_fs13.existsSync)(packageJsonPath)) continue;
3127
+ const packageJson = JSON.parse((0, import_node_fs13.readFileSync)(packageJsonPath, "utf8"));
2148
3128
  if (packageJson.version) return packageJson.version;
2149
3129
  }
2150
3130
  return "unknown";
@@ -2243,14 +3223,14 @@ async function listenWithPortFallback(app, preferredPort) {
2243
3223
  throw new Error(`Could not find an available port starting from ${preferredPort}`);
2244
3224
  }
2245
3225
  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));
3226
+ if (baseDir) return { baseDir: (0, import_node_path13.resolve)(baseDir), isProduction: true };
3227
+ const moduleDir = (0, import_node_path13.dirname)((0, import_node_url.fileURLToPath)(moduleUrl));
2248
3228
  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 };
3229
+ if (!isProduction) return { baseDir: (0, import_node_path13.resolve)(moduleDir), isProduction: false };
3230
+ if ((0, import_node_path13.basename)(moduleDir) === "server") {
3231
+ return { baseDir: (0, import_node_path13.resolve)((0, import_node_path13.dirname)(moduleDir)), isProduction: true };
2252
3232
  }
2253
- return { baseDir: (0, import_node_path8.resolve)(moduleDir), isProduction: true };
3233
+ return { baseDir: (0, import_node_path13.resolve)(moduleDir), isProduction: true };
2254
3234
  }
2255
3235
  function createApp(_port, baseDir) {
2256
3236
  const app = (0, import_express.default)();
@@ -2262,17 +3242,17 @@ function createApp(_port, baseDir) {
2262
3242
  });
2263
3243
  app.use("/api", router);
2264
3244
  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");
3245
+ const popoverPath = isProduction ? (0, import_node_path13.join)(_baseDir, "client", "popover.html") : (0, import_node_path13.join)(_baseDir, "..", "..", "public", "popover.html");
2266
3246
  app.get("/popover.html", (_req, res, next) => {
2267
- if (!(0, import_node_fs8.existsSync)(popoverPath)) {
3247
+ if (!(0, import_node_fs13.existsSync)(popoverPath)) {
2268
3248
  next();
2269
3249
  return;
2270
3250
  }
2271
- res.type("html").send((0, import_node_fs8.readFileSync)(popoverPath, "utf8"));
3251
+ res.type("html").send((0, import_node_fs13.readFileSync)(popoverPath, "utf8"));
2272
3252
  });
2273
3253
  if (isProduction) {
2274
- const clientPath = (0, import_node_path8.join)(_baseDir, "client");
2275
- const clientIndexPath = (0, import_node_path8.join)(clientPath, "index.html");
3254
+ const clientPath = (0, import_node_path13.join)(_baseDir, "client");
3255
+ const clientIndexPath = (0, import_node_path13.join)(clientPath, "index.html");
2276
3256
  app.use(import_express.default.static(clientPath));
2277
3257
  app.use("{*path}", (_req, res) => {
2278
3258
  res.sendFile(clientIndexPath);
@@ -2294,13 +3274,21 @@ async function main() {
2294
3274
  process.exit(1);
2295
3275
  }
2296
3276
  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, ["."], {
3277
+ const { spawn: spawn2 } = await import("node:child_process");
3278
+ const { resolve: resolve2 } = await import("node:path");
3279
+ const { existsSync: existsSync12 } = await import("node:fs");
3280
+ const moduleDir = (0, import_node_path13.dirname)((0, import_node_url.fileURLToPath)(__esbuild_import_meta_url));
3281
+ const packagedPath = resolve2(moduleDir, "..", "..", "TokenDashSwift", ".build", "debug", "TokenDash");
3282
+ const devPath = resolve2(moduleDir, "..", "..", "TokenDashSwift", ".build", "debug", "TokenDash");
3283
+ const swiftBin = existsSync12(packagedPath) ? packagedPath : devPath;
3284
+ if (!existsSync12(swiftBin)) {
3285
+ console.error('Error: TokenDash Swift binary not found. Run "npm run build:swift" first.');
3286
+ process.exit(1);
3287
+ }
3288
+ const child = spawn2(swiftBin, [], {
2300
3289
  env: {
2301
3290
  ...process.env,
2302
- TOKENDASH_PORT: String(preferredPort),
2303
- TOKENDASH_TRAY: "1"
3291
+ TOKENDASH_PORT: String(preferredPort)
2304
3292
  },
2305
3293
  stdio: "inherit"
2306
3294
  });
@@ -2329,11 +3317,14 @@ async function main() {
2329
3317
  console.log('Development mode - use "npm run dev" for full dev experience');
2330
3318
  }
2331
3319
  if (shouldOpenBrowser) {
2332
- setTimeout(() => {
3320
+ setTimeout(async () => {
2333
3321
  console.log("Opening dashboard in your browser...");
2334
- (0, import_open.default)(`http://localhost:${port}`).catch((err) => {
3322
+ try {
3323
+ const { default: open } = await import("open");
3324
+ await open(`http://localhost:${port}`);
3325
+ } catch (err) {
2335
3326
  console.warn("Could not open browser:", err.message);
2336
- });
3327
+ }
2337
3328
  }, 100);
2338
3329
  } else {
2339
3330
  console.log("Browser auto-open disabled (--no-open)");