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