@zhangferry-dev/tokendash 1.6.2 → 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 +7 -6
- package/dist/daemon.cjs +159 -54
- package/dist/daemon.cjs.map +2 -2
- package/dist/electron-server.cjs +159 -54
- package/dist/electron-server.cjs.map +2 -2
- package/dist/server/index.js +1 -0
- package/dist/server/quota/adapter.d.ts +4 -2
- package/dist/server/quota/adapters/claude.d.ts +2 -0
- package/dist/server/quota/adapters/claude.js +49 -21
- package/dist/server/quota/adapters/codex.d.ts +14 -0
- package/dist/server/quota/adapters/codex.js +65 -27
- package/dist/server/quota/adapters/glm.js +9 -3
- package/dist/server/quota/adapters/kimi.js +4 -2
- package/dist/server/quota/adapters/minimax.js +8 -3
- package/dist/server/quota/index.d.ts +1 -1
- package/dist/server/quota/quotaService.d.ts +7 -1
- package/dist/server/quota/quotaService.js +32 -10
- package/dist/server/quota/types.d.ts +11 -0
- package/dist/server/routes/api.js +19 -0
- package/package.json +4 -2
package/dist/electron-server.cjs
CHANGED
|
@@ -2259,6 +2259,32 @@ var QuotaService = class {
|
|
|
2259
2259
|
}
|
|
2260
2260
|
return this.fetchAll();
|
|
2261
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
|
+
}
|
|
2262
2288
|
async fetchWithTimeout(adapter) {
|
|
2263
2289
|
try {
|
|
2264
2290
|
const snapshot = await withTimeout(adapter.fetch(), this.fetchTimeoutMs, adapter.provider);
|
|
@@ -2270,14 +2296,7 @@ var QuotaService = class {
|
|
|
2270
2296
|
}
|
|
2271
2297
|
}
|
|
2272
2298
|
handleFailure(adapter, err) {
|
|
2273
|
-
|
|
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
|
-
}
|
|
2299
|
+
const status = statusForError(err, this.fetchTimeoutMs);
|
|
2281
2300
|
const stale = this.cache.getStale(adapter.provider);
|
|
2282
2301
|
if (stale) {
|
|
2283
2302
|
return { ...stale, freshness: "stale", status };
|
|
@@ -2292,6 +2311,13 @@ var QuotaService = class {
|
|
|
2292
2311
|
};
|
|
2293
2312
|
}
|
|
2294
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
|
+
}
|
|
2295
2321
|
var TimeoutError = class extends Error {
|
|
2296
2322
|
constructor(provider) {
|
|
2297
2323
|
super(`quota fetch timed out: ${provider}`);
|
|
@@ -2416,8 +2442,7 @@ var codexAdapter = {
|
|
|
2416
2442
|
provider: "codex",
|
|
2417
2443
|
displayName: "OpenAI Codex",
|
|
2418
2444
|
async isConfigured() {
|
|
2419
|
-
|
|
2420
|
-
return await codexBinaryAvailable();
|
|
2445
|
+
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(codexHome(), "auth.json")) && resolveCodexBinary() !== null;
|
|
2421
2446
|
},
|
|
2422
2447
|
async fetch() {
|
|
2423
2448
|
const result = await queryRateLimits();
|
|
@@ -2460,10 +2485,16 @@ function capitalize(s) {
|
|
|
2460
2485
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2461
2486
|
}
|
|
2462
2487
|
async function queryRateLimits() {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
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
|
+
});
|
|
2467
2498
|
const client = new JsonRpcClient(proc);
|
|
2468
2499
|
try {
|
|
2469
2500
|
await client.request("initialize", {
|
|
@@ -2552,24 +2583,44 @@ var JsonRpcClient = class {
|
|
|
2552
2583
|
this.pending.clear();
|
|
2553
2584
|
}
|
|
2554
2585
|
};
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
})
|
|
2572
|
-
|
|
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
|
+
}
|
|
2573
2624
|
}
|
|
2574
2625
|
|
|
2575
2626
|
// src/server/quota/adapters/claude.ts
|
|
@@ -2577,6 +2628,7 @@ var import_node_fs9 = require("node:fs");
|
|
|
2577
2628
|
var import_node_path9 = require("node:path");
|
|
2578
2629
|
var import_node_os9 = require("node:os");
|
|
2579
2630
|
var import_node_child_process3 = require("node:child_process");
|
|
2631
|
+
var import_node_crypto = require("node:crypto");
|
|
2580
2632
|
var claudeAdapter = {
|
|
2581
2633
|
provider: "claude",
|
|
2582
2634
|
displayName: "Claude Code",
|
|
@@ -2645,7 +2697,7 @@ function readClaudeToken() {
|
|
|
2645
2697
|
if ((0, import_node_fs9.existsSync)(credPath)) {
|
|
2646
2698
|
try {
|
|
2647
2699
|
const parsed = JSON.parse((0, import_node_fs9.readFileSync)(credPath, "utf8"));
|
|
2648
|
-
return parsed
|
|
2700
|
+
return extractClaudeAccessToken(parsed);
|
|
2649
2701
|
} catch {
|
|
2650
2702
|
return null;
|
|
2651
2703
|
}
|
|
@@ -2653,33 +2705,55 @@ function readClaudeToken() {
|
|
|
2653
2705
|
return null;
|
|
2654
2706
|
}
|
|
2655
2707
|
function readFromKeychain() {
|
|
2656
|
-
const candidates =
|
|
2708
|
+
const candidates = claudeKeychainServiceNames(process.env.CLAUDE_CONFIG_DIR);
|
|
2657
2709
|
try {
|
|
2658
2710
|
const list = (0, import_node_child_process3.execFileSync)("security", ["dump-keychain"], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
2659
|
-
for (const m of list.matchAll(/"
|
|
2711
|
+
for (const m of list.matchAll(/"svce"<blob>="([^"]*Claude Code-credentials[^"]*)"/g)) {
|
|
2660
2712
|
if (m[1] && !candidates.includes(m[1])) candidates.push(m[1]);
|
|
2661
2713
|
}
|
|
2662
2714
|
} catch {
|
|
2663
2715
|
}
|
|
2716
|
+
const accounts = [safeUsername(), void 0];
|
|
2664
2717
|
for (const name of candidates) {
|
|
2665
|
-
|
|
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;
|
|
2718
|
+
for (const account of accounts) {
|
|
2671
2719
|
try {
|
|
2672
|
-
const
|
|
2673
|
-
|
|
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;
|
|
2674
2731
|
} catch {
|
|
2675
|
-
|
|
2732
|
+
continue;
|
|
2676
2733
|
}
|
|
2677
|
-
} catch {
|
|
2678
|
-
continue;
|
|
2679
2734
|
}
|
|
2680
2735
|
}
|
|
2681
2736
|
return null;
|
|
2682
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
|
+
}
|
|
2683
2757
|
|
|
2684
2758
|
// src/server/quota/adapters/glm.ts
|
|
2685
2759
|
var import_node_fs11 = require("node:fs");
|
|
@@ -2715,8 +2789,8 @@ var glmAdapter = {
|
|
|
2715
2789
|
async isConfigured() {
|
|
2716
2790
|
return !!resolveCredential();
|
|
2717
2791
|
},
|
|
2718
|
-
async fetch() {
|
|
2719
|
-
const cred = resolveCredential();
|
|
2792
|
+
async fetch(options) {
|
|
2793
|
+
const cred = resolveCredential(options?.credential);
|
|
2720
2794
|
if (!cred) {
|
|
2721
2795
|
throw new QuotaError({ state: "not_configured", message: "set ZAI_API_KEY or ZHIPU_API_KEY" });
|
|
2722
2796
|
}
|
|
@@ -2772,7 +2846,13 @@ function classifyFetchError2(err) {
|
|
|
2772
2846
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2773
2847
|
return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
|
|
2774
2848
|
}
|
|
2775
|
-
function resolveCredential() {
|
|
2849
|
+
function resolveCredential(proposed) {
|
|
2850
|
+
if (proposed?.apiKey) {
|
|
2851
|
+
return {
|
|
2852
|
+
key: proposed.apiKey,
|
|
2853
|
+
base: proposed.baseUrl || "https://open.bigmodel.cn"
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2776
2856
|
const stored = readStoredCredential("glm");
|
|
2777
2857
|
if (stored) return { key: stored.apiKey, base: stored.baseUrl || "https://open.bigmodel.cn" };
|
|
2778
2858
|
const zai = envOrConfig("ZAI_API_KEY");
|
|
@@ -2827,8 +2907,8 @@ var minimaxAdapter = {
|
|
|
2827
2907
|
async isConfigured() {
|
|
2828
2908
|
return !!resolveCredential2();
|
|
2829
2909
|
},
|
|
2830
|
-
async fetch() {
|
|
2831
|
-
const cred = resolveCredential2();
|
|
2910
|
+
async fetch(options) {
|
|
2911
|
+
const cred = resolveCredential2(options?.credential);
|
|
2832
2912
|
if (!cred) {
|
|
2833
2913
|
throw new QuotaError({ state: "not_configured", message: "set MINIMAX_API_KEY (Subscription Key)" });
|
|
2834
2914
|
}
|
|
@@ -2887,7 +2967,12 @@ function classifyFetchError3(err) {
|
|
|
2887
2967
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2888
2968
|
return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
|
|
2889
2969
|
}
|
|
2890
|
-
function resolveCredential2() {
|
|
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
|
+
}
|
|
2891
2976
|
const stored = readStoredCredential("minimax");
|
|
2892
2977
|
if (stored) {
|
|
2893
2978
|
const region2 = (process.env.MINIMAX_REGION || "").toLowerCase();
|
|
@@ -2915,9 +3000,9 @@ var kimiAdapter = {
|
|
|
2915
3000
|
const cred = readCredentials();
|
|
2916
3001
|
return !!cred && !!cred.access_token;
|
|
2917
3002
|
},
|
|
2918
|
-
async fetch() {
|
|
3003
|
+
async fetch(options) {
|
|
2919
3004
|
const credPath = credentialsPath();
|
|
2920
|
-
let cred = readCredentials();
|
|
3005
|
+
let cred = options?.credential?.apiKey ? { access_token: options.credential.apiKey, token_type: "Bearer" } : readCredentials();
|
|
2921
3006
|
if (!cred || !cred.access_token) {
|
|
2922
3007
|
throw new QuotaError({ state: "not_configured", message: "run `kimi` to log in first" });
|
|
2923
3008
|
}
|
|
@@ -3069,6 +3154,24 @@ async function getQuota(_req, res) {
|
|
|
3069
3154
|
res.status(500).json({ error: "Failed to fetch quota", hint: message });
|
|
3070
3155
|
}
|
|
3071
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
|
+
}
|
|
3072
3175
|
function getAgents(_req, res) {
|
|
3073
3176
|
try {
|
|
3074
3177
|
const agents = detectAvailableAgents();
|
|
@@ -3102,6 +3205,7 @@ function registerApiRoutes(router, appInfo) {
|
|
|
3102
3205
|
router.get("/blocks", getBlocks);
|
|
3103
3206
|
router.get("/analytics", getAnalytics);
|
|
3104
3207
|
router.get("/quota", getQuota);
|
|
3208
|
+
router.post("/quota/validate", validateQuotaCredential);
|
|
3105
3209
|
}
|
|
3106
3210
|
|
|
3107
3211
|
// src/server/index.ts
|
|
@@ -3235,6 +3339,7 @@ function resolveStaticAssetBaseDir(moduleUrl = __esbuild_import_meta_url, baseDi
|
|
|
3235
3339
|
function createApp(_port, baseDir) {
|
|
3236
3340
|
const app = (0, import_express.default)();
|
|
3237
3341
|
const router = import_express.default.Router();
|
|
3342
|
+
app.use(import_express.default.json({ limit: "16kb" }));
|
|
3238
3343
|
registerApiRoutes(router, {
|
|
3239
3344
|
packageName: PACKAGE_NAME,
|
|
3240
3345
|
version: getPackageVersion(),
|