@zhangferry-dev/tokendash 1.6.0 → 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.
- package/README.md +146 -83
- package/dist/client/assets/index-Bw503sNp.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/client/popover.html +4 -3
- package/dist/daemon.cjs +3306 -0
- package/dist/daemon.cjs.map +7 -0
- package/dist/electron-server.cjs +1043 -27
- package/dist/electron-server.cjs.map +4 -4
- package/dist/server/daemon.d.ts +12 -0
- package/dist/server/daemon.js +176 -0
- package/dist/server/index.js +39 -13
- package/dist/server/insightsCalculator.d.ts +15 -0
- package/dist/server/insightsCalculator.js +276 -0
- package/dist/server/quota/adapter.d.ts +47 -0
- package/dist/server/quota/adapter.js +41 -0
- package/dist/server/quota/adapters/claude.d.ts +2 -0
- package/dist/server/quota/adapters/claude.js +124 -0
- package/dist/server/quota/adapters/codex.d.ts +2 -0
- package/dist/server/quota/adapters/codex.js +188 -0
- package/dist/server/quota/adapters/glm.d.ts +2 -0
- package/dist/server/quota/adapters/glm.js +133 -0
- package/dist/server/quota/adapters/kimi.d.ts +2 -0
- package/dist/server/quota/adapters/kimi.js +184 -0
- package/dist/server/quota/adapters/minimax.d.ts +2 -0
- package/dist/server/quota/adapters/minimax.js +77 -0
- package/dist/server/quota/cache.d.ts +20 -0
- package/dist/server/quota/cache.js +44 -0
- package/dist/server/quota/credentialsFile.d.ts +13 -0
- package/dist/server/quota/credentialsFile.js +23 -0
- package/dist/server/quota/helpers.d.ts +39 -0
- package/dist/server/quota/helpers.js +93 -0
- package/dist/server/quota/index.d.ts +5 -0
- package/dist/server/quota/index.js +23 -0
- package/dist/server/quota/quotaService.d.ts +37 -0
- package/dist/server/quota/quotaService.js +141 -0
- package/dist/server/quota/schemas.d.ts +358 -0
- package/dist/server/quota/schemas.js +53 -0
- package/dist/server/quota/types.d.ts +65 -0
- package/dist/server/quota/types.js +10 -0
- package/dist/server/routes/api.d.ts +6 -1
- package/dist/server/routes/api.js +26 -1
- package/dist/server/routes/insights.d.ts +2 -0
- package/dist/server/routes/insights.js +155 -0
- package/package.json +6 -10
- package/resources/icon-1024.png +0 -0
- package/resources/icon.icns +0 -0
- package/resources/icon.png +0 -0
- package/resources/product_menu.png +0 -0
- package/resources/readme-hero.png +0 -0
- package/dist/client/assets/index-_yA9tOzZ.css +0 -1
- package/electron/main.cjs +0 -490
- package/electron/main.js +0 -291
- package/electron/preload.cjs +0 -36
- package/electron/trayBadge.cjs +0 -27
- package/electron/trayBadge.js +0 -30
- package/electron/trayHelper +0 -0
- package/electron/trayHelper.swift +0 -152
- package/electron/updateService.cjs +0 -148
- package/electron-builder.yml +0 -20
- /package/dist/client/assets/{index-CY4G_b0x.js → index-C913wKtU.js} +0 -0
package/dist/electron-server.cjs
CHANGED
|
@@ -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
|
|
40
|
+
var import_node_fs13 = require("node:fs");
|
|
41
41
|
var import_node_url = require("node:url");
|
|
42
|
-
var
|
|
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();
|
|
@@ -2103,7 +3083,17 @@ function getAgents(_req, res) {
|
|
|
2103
3083
|
res.status(500).json({ error: "Failed to detect agents", hint: message });
|
|
2104
3084
|
}
|
|
2105
3085
|
}
|
|
2106
|
-
function
|
|
3086
|
+
function getAppInfo(info) {
|
|
3087
|
+
return (req, res) => {
|
|
3088
|
+
const host = req.get("host");
|
|
3089
|
+
res.json({
|
|
3090
|
+
...info,
|
|
3091
|
+
dashboardUrl: host ? `${req.protocol}://${host}` : info.dashboardUrl
|
|
3092
|
+
});
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
function registerApiRoutes(router, appInfo) {
|
|
3096
|
+
router.get("/app-info", getAppInfo(appInfo));
|
|
2107
3097
|
router.get("/agents", getAgents);
|
|
2108
3098
|
router.get("/daily", getDaily);
|
|
2109
3099
|
router.get("/monthly", getMonthly);
|
|
@@ -2111,10 +3101,10 @@ function registerApiRoutes(router) {
|
|
|
2111
3101
|
router.get("/projects", getProjects);
|
|
2112
3102
|
router.get("/blocks", getBlocks);
|
|
2113
3103
|
router.get("/analytics", getAnalytics);
|
|
3104
|
+
router.get("/quota", getQuota);
|
|
2114
3105
|
}
|
|
2115
3106
|
|
|
2116
3107
|
// src/server/index.ts
|
|
2117
|
-
var import_open = __toESM(require("open"), 1);
|
|
2118
3108
|
var CLI_USAGE = [
|
|
2119
3109
|
"Usage:",
|
|
2120
3110
|
" tokendash",
|
|
@@ -2122,11 +3112,22 @@ var CLI_USAGE = [
|
|
|
2122
3112
|
" tokendash --port <number> [--no-open]",
|
|
2123
3113
|
" tokendash --tray [--port <number>]"
|
|
2124
3114
|
].join("\n");
|
|
3115
|
+
var PACKAGE_NAME = "@zhangferry-dev/tokendash";
|
|
2125
3116
|
function getPackageVersion() {
|
|
2126
3117
|
const __filename = (0, import_node_url.fileURLToPath)(__esbuild_import_meta_url);
|
|
2127
|
-
const __dirname = (0,
|
|
2128
|
-
const
|
|
2129
|
-
|
|
3118
|
+
const __dirname = (0, import_node_path13.dirname)(__filename);
|
|
3119
|
+
const packageJsonPaths = [
|
|
3120
|
+
(0, import_node_path13.join)(__dirname, "..", "..", "package.json"),
|
|
3121
|
+
// dist/server/index.js
|
|
3122
|
+
(0, import_node_path13.join)(__dirname, "..", "package.json")
|
|
3123
|
+
// bundled server entrypoint
|
|
3124
|
+
];
|
|
3125
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
3126
|
+
if (!(0, import_node_fs13.existsSync)(packageJsonPath)) continue;
|
|
3127
|
+
const packageJson = JSON.parse((0, import_node_fs13.readFileSync)(packageJsonPath, "utf8"));
|
|
3128
|
+
if (packageJson.version) return packageJson.version;
|
|
3129
|
+
}
|
|
3130
|
+
return "unknown";
|
|
2130
3131
|
}
|
|
2131
3132
|
function exitWithCliError(message) {
|
|
2132
3133
|
console.error(message);
|
|
@@ -2222,32 +3223,36 @@ async function listenWithPortFallback(app, preferredPort) {
|
|
|
2222
3223
|
throw new Error(`Could not find an available port starting from ${preferredPort}`);
|
|
2223
3224
|
}
|
|
2224
3225
|
function resolveStaticAssetBaseDir(moduleUrl = __esbuild_import_meta_url, baseDir) {
|
|
2225
|
-
if (baseDir) return { baseDir: (0,
|
|
2226
|
-
const moduleDir = (0,
|
|
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));
|
|
2227
3228
|
const isProduction = moduleUrl.includes("/dist/");
|
|
2228
|
-
if (!isProduction) return { baseDir: (0,
|
|
2229
|
-
if ((0,
|
|
2230
|
-
return { baseDir: (0,
|
|
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 };
|
|
2231
3232
|
}
|
|
2232
|
-
return { baseDir: (0,
|
|
3233
|
+
return { baseDir: (0, import_node_path13.resolve)(moduleDir), isProduction: true };
|
|
2233
3234
|
}
|
|
2234
3235
|
function createApp(_port, baseDir) {
|
|
2235
3236
|
const app = (0, import_express.default)();
|
|
2236
3237
|
const router = import_express.default.Router();
|
|
2237
|
-
registerApiRoutes(router
|
|
3238
|
+
registerApiRoutes(router, {
|
|
3239
|
+
packageName: PACKAGE_NAME,
|
|
3240
|
+
version: getPackageVersion(),
|
|
3241
|
+
dashboardUrl: `http://localhost:${resolvePort(_port)}`
|
|
3242
|
+
});
|
|
2238
3243
|
app.use("/api", router);
|
|
2239
3244
|
const { baseDir: _baseDir, isProduction } = resolveStaticAssetBaseDir(__esbuild_import_meta_url, baseDir);
|
|
2240
|
-
const popoverPath = isProduction ? (0,
|
|
3245
|
+
const popoverPath = isProduction ? (0, import_node_path13.join)(_baseDir, "client", "popover.html") : (0, import_node_path13.join)(_baseDir, "..", "..", "public", "popover.html");
|
|
2241
3246
|
app.get("/popover.html", (_req, res, next) => {
|
|
2242
|
-
if (!(0,
|
|
3247
|
+
if (!(0, import_node_fs13.existsSync)(popoverPath)) {
|
|
2243
3248
|
next();
|
|
2244
3249
|
return;
|
|
2245
3250
|
}
|
|
2246
|
-
res.type("html").send((0,
|
|
3251
|
+
res.type("html").send((0, import_node_fs13.readFileSync)(popoverPath, "utf8"));
|
|
2247
3252
|
});
|
|
2248
3253
|
if (isProduction) {
|
|
2249
|
-
const clientPath = (0,
|
|
2250
|
-
const clientIndexPath = (0,
|
|
3254
|
+
const clientPath = (0, import_node_path13.join)(_baseDir, "client");
|
|
3255
|
+
const clientIndexPath = (0, import_node_path13.join)(clientPath, "index.html");
|
|
2251
3256
|
app.use(import_express.default.static(clientPath));
|
|
2252
3257
|
app.use("{*path}", (_req, res) => {
|
|
2253
3258
|
res.sendFile(clientIndexPath);
|
|
@@ -2269,13 +3274,21 @@ async function main() {
|
|
|
2269
3274
|
process.exit(1);
|
|
2270
3275
|
}
|
|
2271
3276
|
console.log(`Starting tokendash v${version} in tray mode...`);
|
|
2272
|
-
const {
|
|
2273
|
-
const {
|
|
2274
|
-
const
|
|
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, [], {
|
|
2275
3289
|
env: {
|
|
2276
3290
|
...process.env,
|
|
2277
|
-
TOKENDASH_PORT: String(preferredPort)
|
|
2278
|
-
TOKENDASH_TRAY: "1"
|
|
3291
|
+
TOKENDASH_PORT: String(preferredPort)
|
|
2279
3292
|
},
|
|
2280
3293
|
stdio: "inherit"
|
|
2281
3294
|
});
|
|
@@ -2304,11 +3317,14 @@ async function main() {
|
|
|
2304
3317
|
console.log('Development mode - use "npm run dev" for full dev experience');
|
|
2305
3318
|
}
|
|
2306
3319
|
if (shouldOpenBrowser) {
|
|
2307
|
-
setTimeout(() => {
|
|
3320
|
+
setTimeout(async () => {
|
|
2308
3321
|
console.log("Opening dashboard in your browser...");
|
|
2309
|
-
|
|
3322
|
+
try {
|
|
3323
|
+
const { default: open } = await import("open");
|
|
3324
|
+
await open(`http://localhost:${port}`);
|
|
3325
|
+
} catch (err) {
|
|
2310
3326
|
console.warn("Could not open browser:", err.message);
|
|
2311
|
-
}
|
|
3327
|
+
}
|
|
2312
3328
|
}, 100);
|
|
2313
3329
|
} else {
|
|
2314
3330
|
console.log("Browser auto-open disabled (--no-open)");
|