tokentracker-cli 0.73.0 → 0.74.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.de.md +3 -2
  2. package/README.ja.md +3 -2
  3. package/README.ko.md +3 -2
  4. package/README.md +3 -2
  5. package/README.zh-CN.md +3 -2
  6. package/dashboard/dist/assets/{ActivityHeatmap-C0txyttw.js → ActivityHeatmap-D5DFI_JT.js} +1 -1
  7. package/dashboard/dist/assets/{Card-BhbsF2sd.js → Card-C0dBE40G.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-DYuw3NEB.js → CommunityStatsModal-EoqdsEND.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-pSNHLvla.js → ConfirmModal-Dmmph0Fh.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-DWzdgrPp.js → DashboardPage-CCj2poZF.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-Bv_Rf-TG.js → DevicePage-DeAi9xZM.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-N80c6k5O.js → DialogClose-D3RkFfxL.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-SXe3669h.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-DhnXV44R.js → DialogTitle-B_MCherE.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-D-iQ_Y2x.js → FadeIn-DNN2K13r.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-BfDCQeWv.js → HeaderGithubStar-lFcC5cJF.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-Dp4obb18.js → IpCheckPage-CeLaiU7v.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-CRjKRU6i.js → LandingPage-DAoPUMvI.js} +97 -37
  19. package/dashboard/dist/assets/{LeaderboardAvatar-Cgn3pcho.js → LeaderboardAvatar-FQi0zZEF.js} +1 -1
  20. package/dashboard/dist/assets/{LeaderboardPage-Bhq7LL3D.js → LeaderboardPage-D5b5VXQC.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardProfileModal-Bsls5fHq.js → LeaderboardProfileModal-B4zRhrdW.js} +1 -1
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-BYfd5SmB.js → LeaderboardProfilePage-Bq8rq1En.js} +1 -1
  23. package/dashboard/dist/assets/{LimitsPage-DAnTAkl9.js → LimitsPage-DWM80BaS.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-CVg_JUKd.js → LocalOnlyNotice-B8m8CUt0.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage-D9neHZav.js → LoginPage-BUW1YAd4.js} +1 -1
  26. package/dashboard/dist/assets/{PopoverPopup-CenmP6bc.js → PopoverPopup-Bpt95eH1.js} +1 -1
  27. package/dashboard/dist/assets/{ResetPasswordPage-BGXobvFW.js → ResetPasswordPage-BbIfq0Oo.js} +1 -1
  28. package/dashboard/dist/assets/{Select-cCMkMvY9.js → Select-gTXpf9KQ.js} +1 -1
  29. package/dashboard/dist/assets/{SelectItemText-OO6aEsCS.js → SelectItemText-DEkgwu_p.js} +1 -1
  30. package/dashboard/dist/assets/{SettingsPage-zFmcMe2k.js → SettingsPage-DI9CabCH.js} +1 -1
  31. package/dashboard/dist/assets/{SkillsPage-wAVoEoqs.js → SkillsPage-BfCZDHaD.js} +1 -1
  32. package/dashboard/dist/assets/{WidgetsPage-CdxVbQxt.js → WidgetsPage-rU0VeGYf.js} +1 -1
  33. package/dashboard/dist/assets/{WrappedPage-HCMET3ON.js → WrappedPage-CZa-5-KZ.js} +1 -1
  34. package/dashboard/dist/assets/{agent-logos-ngQzAIwh.js → agent-logos-EmiprqVo.js} +1 -1
  35. package/dashboard/dist/assets/{arrow-up-right-DwNdVf8C.js → arrow-up-right-k-vRvS_D.js} +1 -1
  36. package/dashboard/dist/assets/check-BBoXV3Xq.js +1 -0
  37. package/dashboard/dist/assets/{copy-D5Tgbfx9.js → copy-syqMmWHW.js} +1 -1
  38. package/dashboard/dist/assets/{download-BkXuG4x3.js → download-DZcuOKDP.js} +1 -1
  39. package/dashboard/dist/assets/{format-tokens-DpplPif4.js → format-tokens-D7k8OXQS.js} +1 -1
  40. package/dashboard/dist/assets/{info-bmDUStkp.js → info-CEBgCIZx.js} +1 -1
  41. package/dashboard/dist/assets/main--nUrKVF3.css +1 -0
  42. package/dashboard/dist/assets/{main-COdyqkJx.js → main-BALrkVr_.js} +2 -2
  43. package/dashboard/dist/assets/{use-community-stats-BSqhgTim.js → use-community-stats-DUttYzRH.js} +1 -1
  44. package/dashboard/dist/assets/{use-limits-display-prefs-DaL_M4gM.js → use-limits-display-prefs-BhIvYvqB.js} +1 -1
  45. package/dashboard/dist/assets/{use-native-settings-DO1hquUr.js → use-native-settings-BhOpUBnt.js} +1 -1
  46. package/dashboard/dist/assets/{use-ordered-list-Dj5XtHo4.js → use-ordered-list-Cd6yagUF.js} +1 -1
  47. package/dashboard/dist/assets/{use-quality-per-dollar-pref-TbW0hepx.js → use-quality-per-dollar-pref-DBeuBvln.js} +1 -1
  48. package/dashboard/dist/assets/{use-usage-limits-BUth5S9D.js → use-usage-limits-DBhYqpBW.js} +1 -1
  49. package/dashboard/dist/assets/{useCurrency-BmoIPMdl.js → useCurrency-Cy2Q8nWt.js} +1 -1
  50. package/dashboard/dist/assets/{useScrollLock-BYRX6K6Z.js → useScrollLock-C6Eqqoge.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dashboard/dist/share.html +2 -2
  53. package/package.json +1 -1
  54. package/src/commands/status.js +47 -4
  55. package/src/commands/sync.js +72 -18
  56. package/src/lib/local-api.js +7 -2
  57. package/src/lib/pricing/curated-overrides.json +7 -2
  58. package/src/lib/pricing/seed-snapshot.json +1 -1
  59. package/src/lib/rollout.js +385 -0
  60. package/src/lib/sqlite-reader.js +24 -6
  61. package/src/lib/usage-limits.js +224 -29
  62. package/src/lib/zcode-limits.js +573 -39
  63. package/dashboard/dist/assets/DialogDescription-lEwHWbK4.js +0 -1
  64. package/dashboard/dist/assets/check-B2E13r-e.js +0 -1
  65. package/dashboard/dist/assets/main-DXM2UPde.css +0 -1
@@ -1,8 +1,14 @@
1
1
  const fs = require("node:fs");
2
+ const crypto = require("node:crypto");
2
3
  const os = require("node:os");
3
4
  const path = require("node:path");
4
5
 
5
6
  const DEFAULT_BILLING_BASE_URL = "https://zcode.z.ai/api/v1/zcode-plan";
7
+ const DEFAULT_ZAI_MONITOR_BASE_URL = "https://api.z.ai";
8
+ const DEFAULT_BIGMODEL_MONITOR_BASE_URL = "https://bigmodel.cn";
9
+ const ZCODE_MONITOR_QUOTA_PATH = "/api/monitor/usage/quota/limit";
10
+ const DEFAULT_ZCODE_APP_VERSION = "3.2.5";
11
+ const DEFAULT_ZCODE_LOG_FALLBACK_MAX_AGE_MS = 6 * 60 * 60 * 1000;
6
12
 
7
13
  function resolveZcodeHome({ home, env = process.env } = {}) {
8
14
  if (typeof env.TOKENTRACKER_ZCODE_HOME === "string" && env.TOKENTRACKER_ZCODE_HOME.trim()) {
@@ -23,6 +29,51 @@ function resolveZcodeBillingBaseUrl(env = process.env) {
23
29
  return DEFAULT_BILLING_BASE_URL;
24
30
  }
25
31
 
32
+ function parsePositiveInteger(value, fallback) {
33
+ const n = Number(value);
34
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
35
+ }
36
+
37
+ function readZcodeAppVersionFromPlist(plistPath) {
38
+ try {
39
+ if (!fs.existsSync(plistPath)) return null;
40
+ const text = fs.readFileSync(plistPath, "utf8");
41
+ const keyIndex = text.indexOf("<key>CFBundleShortVersionString</key>");
42
+ if (keyIndex < 0) return null;
43
+ const match = text.slice(keyIndex).match(/<string>([^<]+)<\/string>/);
44
+ const version = typeof match?.[1] === "string" ? match[1].trim() : "";
45
+ return version || null;
46
+ } catch (_error) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function resolveZcodeAppVersion({ home, env = process.env } = {}) {
52
+ const explicit =
53
+ typeof env.TOKENTRACKER_ZCODE_APP_VERSION === "string"
54
+ ? env.TOKENTRACKER_ZCODE_APP_VERSION.trim()
55
+ : "";
56
+ if (explicit) return explicit;
57
+
58
+ const appPath =
59
+ typeof env.TOKENTRACKER_ZCODE_APP_PATH === "string" && env.TOKENTRACKER_ZCODE_APP_PATH.trim()
60
+ ? env.TOKENTRACKER_ZCODE_APP_PATH.trim()
61
+ : null;
62
+ const candidates = appPath
63
+ ? [path.join(appPath, "Contents", "Info.plist")]
64
+ : [
65
+ "/Applications/ZCode.app/Contents/Info.plist",
66
+ path.join(home || os.homedir(), "Applications", "ZCode.app", "Contents", "Info.plist"),
67
+ ];
68
+ for (const plistPath of candidates) {
69
+ const version = readZcodeAppVersionFromPlist(plistPath);
70
+ if (version) return version;
71
+ }
72
+ // The API currently rejects balance requests without app_version. The value is
73
+ // not used for local accounting, so keep a conservative fallback for CLI-only installs.
74
+ return DEFAULT_ZCODE_APP_VERSION;
75
+ }
76
+
26
77
  function isZcodeInstalled({ home, env } = {}) {
27
78
  const zcodeHome = resolveZcodeHome({ home, env });
28
79
  const configPath = path.join(zcodeHome, "v2", "config.json");
@@ -31,34 +82,251 @@ function isZcodeInstalled({ home, env } = {}) {
31
82
  return fs.existsSync(dbPath);
32
83
  }
33
84
 
34
- function loadZcodeApiKey({ home, env } = {}) {
85
+ function loadZcodeProviderAvailability({ home, env } = {}) {
86
+ const zcodeHome = resolveZcodeHome({ home, env });
87
+ const cachePath = path.join(zcodeHome, "v2", "coding-plan-cache.json");
88
+ if (!fs.existsSync(cachePath)) return {};
89
+ try {
90
+ const cache = JSON.parse(fs.readFileSync(cachePath, "utf8"));
91
+ const items = cache?.entryStatus?.items;
92
+ return items && typeof items === "object" ? items : {};
93
+ } catch (_error) {
94
+ return {};
95
+ }
96
+ }
97
+
98
+ function loadZcodeSelectedPlanProviderKeys({ home, env } = {}) {
99
+ const zcodeHome = resolveZcodeHome({ home, env });
100
+ const settingPath = path.join(zcodeHome, "v2", "setting.json");
101
+ if (!fs.existsSync(settingPath)) return [];
102
+ try {
103
+ const setting = JSON.parse(fs.readFileSync(settingPath, "utf8"));
104
+ const selected = setting?.modelProviderFamilySelectedKeys;
105
+ if (!selected || typeof selected !== "object") return [];
106
+ const domain = typeof setting?.providerFamilyDomain === "string" ? setting.providerFamilyDomain : "";
107
+ const domains = [domain, ...Object.keys(selected)].filter(Boolean);
108
+ const out = [];
109
+ for (const key of domains) {
110
+ const raw = typeof selected[key] === "string" ? selected[key].trim() : "";
111
+ const match = raw.match(/builtin:(?:bigmodel|zai)-(?:start|coding)-plan/);
112
+ if (match && !out.includes(match[0])) out.push(match[0]);
113
+ }
114
+ return out;
115
+ } catch (_error) {
116
+ return [];
117
+ }
118
+ }
119
+
120
+ function resolveZcodeCredentialsPath({ home, env } = {}) {
121
+ return path.join(resolveZcodeHome({ home, env }), "v2", "credentials.json");
122
+ }
123
+
124
+ function createZcodeCredentialSecret({ home, env = process.env } = {}) {
125
+ if (typeof env.ZCODE_CREDENTIAL_SECRET === "string" && env.ZCODE_CREDENTIAL_SECRET) {
126
+ return env.ZCODE_CREDENTIAL_SECRET;
127
+ }
128
+ let username = "";
129
+ try {
130
+ username = os.userInfo().username || "";
131
+ } catch (_error) {
132
+ username = "";
133
+ }
134
+ return `zcode-credential-fallback:${process.platform}:${home || os.homedir()}:${username}`;
135
+ }
136
+
137
+ function decryptZcodeCredentialValue(value, { home, env } = {}) {
138
+ if (typeof value !== "string") return null;
139
+ if (!value.startsWith("enc:v1:")) return value;
140
+ const encoded = value.slice("enc:v1:".length);
141
+ const parts = encoded.split(".");
142
+ if (parts.length !== 3) return null;
143
+ try {
144
+ const [ivPart, tagPart, encryptedPart] = parts;
145
+ const iv = Buffer.from(ivPart, "base64url");
146
+ const encrypted = Buffer.from(encryptedPart, "base64url");
147
+ const tag = Buffer.from(tagPart, "base64url");
148
+ const key = crypto.createHash("sha256").update(createZcodeCredentialSecret({ home, env })).digest();
149
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
150
+ decipher.setAuthTag(tag);
151
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
152
+ } catch (_error) {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function loadZcodeCredentials({ home, env } = {}) {
158
+ const credentialsPath = resolveZcodeCredentialsPath({ home, env });
159
+ if (!fs.existsSync(credentialsPath)) return {};
160
+ try {
161
+ const raw = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
162
+ return raw && typeof raw === "object" ? raw : {};
163
+ } catch (_error) {
164
+ return {};
165
+ }
166
+ }
167
+
168
+ function loadZcodeCredential(name, { home, env } = {}) {
169
+ const credentials = loadZcodeCredentials({ home, env });
170
+ const decrypted = decryptZcodeCredentialValue(credentials?.[name], { home, env });
171
+ return typeof decrypted === "string" && decrypted.trim() ? decrypted.trim() : "";
172
+ }
173
+
174
+ function loadZcodeActiveProvider({ home, env } = {}) {
175
+ return loadZcodeCredential("oauth:active_provider", { home, env });
176
+ }
177
+
178
+ function resolveZcodeCredentialAuth(providerKey, { home, env } = {}) {
179
+ const activeProvider = loadZcodeActiveProvider({ home, env });
180
+ if (
181
+ (providerKey === "builtin:zai-start-plan" && activeProvider === "zai") ||
182
+ (providerKey === "builtin:bigmodel-start-plan" && activeProvider === "bigmodel")
183
+ ) {
184
+ return loadZcodeCredential("zcodejwttoken", { home, env });
185
+ }
186
+ return "";
187
+ }
188
+
189
+ function isZcodeBuiltinPlanProvider(providerKey) {
190
+ return /^builtin:(bigmodel|zai)-(start|coding)-plan$/.test(providerKey);
191
+ }
192
+
193
+ function isZcodeCodingPlanProvider(providerKey) {
194
+ return /^builtin:(bigmodel|zai)-coding-plan$/.test(providerKey);
195
+ }
196
+
197
+ function isZcodeStartPlanProvider(providerKey) {
198
+ return /^builtin:(bigmodel|zai)-start-plan$/.test(providerKey);
199
+ }
200
+
201
+ function readEnvString(env, names) {
202
+ for (const name of names) {
203
+ const value = typeof env?.[name] === "string" ? env[name].trim() : "";
204
+ if (value) return value;
205
+ }
206
+ return "";
207
+ }
208
+
209
+ function resolveZcodeProviderBillingBaseUrl(providerKey, provider, env = process.env) {
210
+ const explicit = resolveZcodeBillingBaseUrl(env);
211
+ if (explicit !== DEFAULT_BILLING_BASE_URL) return explicit;
212
+ if (isZcodeStartPlanProvider(providerKey)) return DEFAULT_BILLING_BASE_URL;
213
+ const baseUrl = typeof provider?.options?.baseURL === "string" ? provider.options.baseURL.trim() : "";
214
+ if (/\/zcode-plan\/anthropic\/?$/i.test(baseUrl)) return baseUrl.replace(/\/anthropic\/?$/i, "");
215
+ return null;
216
+ }
217
+
218
+ function normalizeUrlOrigin(value) {
219
+ try {
220
+ const url = new URL(value);
221
+ return url.origin.replace(/\/$/, "");
222
+ } catch (_error) {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ function resolveZcodeProviderQuotaUrl(providerKey, provider, env = process.env) {
228
+ if (!isZcodeCodingPlanProvider(providerKey)) return null;
229
+ const explicit = readEnvString(env, ["TOKENTRACKER_ZCODE_MONITOR_QUOTA_URL"]);
230
+ if (explicit) return explicit;
231
+
232
+ const baseUrl = typeof provider?.options?.baseURL === "string" ? provider.options.baseURL.trim() : "";
233
+ const baseOrigin = normalizeUrlOrigin(baseUrl);
234
+ const isZai = providerKey === "builtin:zai-coding-plan";
235
+ const origin = isZai
236
+ ? readEnvString(env, [
237
+ "TOKENTRACKER_ZCODE_ZAI_MONITOR_BASE_URL",
238
+ "ZAI_BUSINESS_BASE_URL",
239
+ "ZAI_PRODUCTION_BUSINESS_BASE_URL",
240
+ ]) || (baseOrigin && /(^|\.)api\.z\.ai$/i.test(new URL(baseOrigin).hostname) ? baseOrigin : DEFAULT_ZAI_MONITOR_BASE_URL)
241
+ : readEnvString(env, [
242
+ "TOKENTRACKER_ZCODE_BIGMODEL_MONITOR_BASE_URL",
243
+ "BIGMODEL_API_BASE_URL",
244
+ "BIGMODEL_PRODUCTION_API_BASE_URL",
245
+ ]) || DEFAULT_BIGMODEL_MONITOR_BASE_URL;
246
+ return `${origin.replace(/\/$/, "")}${ZCODE_MONITOR_QUOTA_PATH}`;
247
+ }
248
+
249
+ function loadZcodeAuthCandidates({ home, env } = {}) {
35
250
  const zcodeHome = resolveZcodeHome({ home, env });
36
251
  const configPath = path.join(zcodeHome, "v2", "config.json");
37
- if (!fs.existsSync(configPath)) return null;
252
+ if (!fs.existsSync(configPath)) return [];
38
253
  try {
39
254
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
40
- if (!config || typeof config !== "object") return null;
255
+ if (!config || typeof config !== "object") return [];
41
256
  const providers = config.provider || {};
42
- // Try the active start-plan first, then coding-plan variants
43
- const candidates = [
257
+ const defaultCandidates = [
44
258
  "builtin:bigmodel-start-plan",
45
259
  "builtin:zai-start-plan",
46
260
  "builtin:bigmodel-coding-plan",
47
261
  "builtin:zai-coding-plan",
48
262
  ];
263
+ const availability = loadZcodeProviderAvailability({ home, env });
264
+ const hasAvailability = Object.keys(availability).length > 0;
265
+ const selectedCandidates = loadZcodeSelectedPlanProviderKeys({ home, env })
266
+ .filter((key) => defaultCandidates.includes(key));
267
+ const availableCandidates = defaultCandidates.filter((key) => availability?.[key]?.status === "available");
268
+ const candidates = [
269
+ ...selectedCandidates,
270
+ ...availableCandidates,
271
+ ...defaultCandidates,
272
+ ].filter((key, index, all) => all.indexOf(key) === index);
273
+ const auths = [];
49
274
  for (const key of candidates) {
50
275
  const provider = providers[key];
51
276
  if (!provider || typeof provider !== "object") continue;
52
277
  if (provider.enabled === false) continue;
278
+ if (hasAvailability && availability?.[key]?.status && availability[key].status !== "available") continue;
53
279
  const apiKey = typeof provider?.options?.apiKey === "string" ? provider.options.apiKey.trim() : "";
54
- if (apiKey) return { apiKey, providerKey: key, baseUrl: provider?.options?.baseURL || null };
280
+ const billingBaseUrl = resolveZcodeProviderBillingBaseUrl(key, provider, env);
281
+ const quotaUrl = resolveZcodeProviderQuotaUrl(key, provider, env);
282
+ const credentialApiKey = resolveZcodeCredentialAuth(key, { home, env });
283
+ const authEntries = [
284
+ credentialApiKey ? { apiKey: credentialApiKey, authSource: "credential:zcodejwttoken" } : null,
285
+ apiKey ? { apiKey, authSource: "provider:config" } : null,
286
+ ].filter(Boolean);
287
+ const seenKeys = new Set();
288
+ for (const entry of authEntries) {
289
+ if ((!billingBaseUrl && !quotaUrl) || seenKeys.has(entry.apiKey)) continue;
290
+ seenKeys.add(entry.apiKey);
291
+ auths.push({
292
+ apiKey: entry.apiKey,
293
+ auth_source: entry.authSource,
294
+ providerKey: key,
295
+ planKind: isZcodeCodingPlanProvider(key) ? "coding-plan" : "start-plan",
296
+ baseUrl: provider?.options?.baseURL || null,
297
+ billingBaseUrl,
298
+ quotaUrl,
299
+ availability: availability?.[key]?.status || null,
300
+ });
301
+ }
55
302
  }
56
- return null;
303
+ return auths;
57
304
  } catch (_error) {
58
- return null;
305
+ return [];
59
306
  }
60
307
  }
61
308
 
309
+ function loadZcodeApiKey({ home, env } = {}) {
310
+ return loadZcodeAuthCandidates({ home, env })[0] || null;
311
+ }
312
+
313
+ function buildZcodeSourceHeaders({ home, env } = {}) {
314
+ const headers = {
315
+ "User-Agent": `ZCode/${resolveZcodeAppVersion({ home, env })}`,
316
+ "HTTP-Referer": "https://zcode.z.ai/",
317
+ "X-ZCode-App-Version": resolveZcodeAppVersion({ home, env }),
318
+ "X-Platform": process.platform,
319
+ "X-Release-Channel": "stable",
320
+ "X-Client-Language": Intl.DateTimeFormat().resolvedOptions().locale || "en-US",
321
+ "X-Client-Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
322
+ "X-Os-Category": process.platform,
323
+ "X-Os-Version": os.release(),
324
+ };
325
+ const deviceMid = loadZcodeCredential("zcodefeedbackclientid", { home, env });
326
+ if (deviceMid) headers["X-Device-Mid"] = deviceMid;
327
+ return headers;
328
+ }
329
+
62
330
  function zcodeValNumber(value) {
63
331
  if (value == null) return null;
64
332
  const n = Number(value);
@@ -108,7 +376,14 @@ function normalizeZcodeBalanceResponse(body) {
108
376
 
109
377
  const balances = Array.isArray(data.balances) ? data.balances : [];
110
378
  if (!balances.length) {
111
- throw new Error("Could not parse ZCode balance: no balance buckets");
379
+ return {
380
+ server_time: zcodeValNumber(data.server_time),
381
+ plan_id: null,
382
+ plan_label: null,
383
+ buckets: [],
384
+ primary_window: null,
385
+ secondary_window: null,
386
+ };
112
387
  }
113
388
 
114
389
  const serverTime = zcodeValNumber(data.server_time);
@@ -150,64 +425,323 @@ function normalizeZcodeBalanceResponse(body) {
150
425
  };
151
426
  }
152
427
 
153
- async function fetchZcodeBilling(apiKey, { fetchImpl = fetch, baseUrl, env } = {}) {
428
+ async function fetchZcodeBilling(apiKey, { fetchImpl = fetch, baseUrl, env, home } = {}) {
154
429
  const root = (baseUrl || resolveZcodeBillingBaseUrl(env)).replace(/\/$/, "");
155
- const res = await fetchImpl(`${root}/billing/balance`, {
430
+ const url = new URL(`${root}/billing/balance`);
431
+ const appVersion = resolveZcodeAppVersion({ home, env });
432
+ if (appVersion) url.searchParams.set("app_version", appVersion);
433
+ const headers = {
434
+ Authorization: `Bearer ${apiKey}`,
435
+ Accept: "application/json",
436
+ };
437
+ if (url.origin === new URL(DEFAULT_BILLING_BASE_URL).origin) {
438
+ Object.assign(headers, buildZcodeSourceHeaders({ home, env }));
439
+ }
440
+ const res = await fetchImpl(url.toString(), {
441
+ method: "GET",
442
+ headers,
443
+ });
444
+ if (res.status === 401 || res.status === 403) {
445
+ throw new Error("Not authenticated with ZCode. Run `zcode` in Terminal to log in.");
446
+ }
447
+ if (!res.ok) {
448
+ let detail = "";
449
+ try {
450
+ const body = await res.json();
451
+ if (body && typeof body === "object") {
452
+ const code = body.code != null ? ` code=${body.code}` : "";
453
+ const msg = body.msg ? ` msg=${body.msg}` : "";
454
+ detail = `${code}${msg}`;
455
+ }
456
+ } catch (_error) {
457
+ detail = "";
458
+ }
459
+ throw new Error(`ZCode billing API returned ${res.status}${detail}`);
460
+ }
461
+ return res.json();
462
+ }
463
+
464
+ async function fetchZcodeCodingPlanQuota(apiKey, { fetchImpl = fetch, quotaUrl } = {}) {
465
+ const res = await fetchImpl(quotaUrl, {
156
466
  method: "GET",
157
467
  headers: {
158
- Authorization: `Bearer ${apiKey}`,
468
+ authorization: apiKey,
159
469
  Accept: "application/json",
160
470
  },
161
471
  });
162
472
  if (res.status === 401 || res.status === 403) {
163
- throw new Error("Not authenticated with ZCode. Run `zcode` in Terminal to log in.");
473
+ throw new Error("Not authenticated with ZCode coding plan. Run `zcode` in Terminal to log in.");
474
+ }
475
+ let body = null;
476
+ try {
477
+ body = await res.json();
478
+ } catch (_error) {
479
+ body = null;
164
480
  }
165
481
  if (!res.ok) {
166
- throw new Error(`ZCode billing API returned ${res.status}`);
482
+ const code = body?.code != null ? ` code=${body.code}` : "";
483
+ const msg = body?.msg || body?.message ? ` msg=${body.msg || body.message}` : "";
484
+ throw new Error(`ZCode coding plan API returned ${res.status}${code}${msg}`);
167
485
  }
168
- return res.json();
486
+ return body;
487
+ }
488
+
489
+ function quotaTimestampToIso(value) {
490
+ if (typeof value === "string" && value.trim()) {
491
+ const parsed = new Date(value.trim());
492
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
493
+ }
494
+ const n = zcodeValNumber(value);
495
+ if (n == null || n <= 0) return null;
496
+ return new Date(n > 10_000_000_000 ? n : n * 1000).toISOString();
497
+ }
498
+
499
+ function normalizeZcodeQuotaLimit(limit) {
500
+ if (!limit || typeof limit !== "object") return null;
501
+ const total = zcodeValNumber(limit.number) ?? zcodeValNumber(limit.unit);
502
+ const used = zcodeValNumber(limit.usage) ?? zcodeValNumber(limit.currentValue);
503
+ const remaining = zcodeValNumber(limit.remaining);
504
+ const rawPercentage = zcodeValNumber(limit.percentage);
505
+ let usedPercent = null;
506
+ if (total != null && total > 0 && used != null) {
507
+ usedPercent = (used / total) * 100;
508
+ } else if (total != null && total > 0 && remaining != null) {
509
+ usedPercent = ((total - remaining) / total) * 100;
510
+ } else if (rawPercentage != null) {
511
+ usedPercent = rawPercentage <= 1 ? rawPercentage * 100 : rawPercentage;
512
+ }
513
+
514
+ const detail = Array.isArray(limit.usageDetails) ? limit.usageDetails.find((item) => item && typeof item === "object") : null;
515
+ const showName =
516
+ (typeof detail?.displayName === "string" && detail.displayName.trim())
517
+ || (typeof detail?.modelCode === "string" && detail.modelCode.trim())
518
+ || (typeof limit.type === "string" && limit.type.trim())
519
+ || "Coding plan";
520
+
521
+ return {
522
+ show_name: showName,
523
+ entitlement_id: typeof limit.type === "string" ? limit.type : "",
524
+ total_units: total,
525
+ used_units: used,
526
+ remaining_units: remaining,
527
+ window: buildWindow({ usedPercent, resetAt: quotaTimestampToIso(limit.nextResetTime) }),
528
+ };
529
+ }
530
+
531
+ function normalizeZcodeCodingPlanQuotaResponse(body) {
532
+ const code = typeof body?.code === "number" ? body.code : null;
533
+ if (code !== null && code !== 0 && code !== 200) {
534
+ throw new Error(`ZCode coding plan API error: code=${code} msg=${body?.msg || body?.message || "unknown"}`);
535
+ }
536
+ if (body?.success === false) {
537
+ throw new Error(`ZCode coding plan API error: msg=${body?.msg || body?.message || "unknown"}`);
538
+ }
539
+ const data = body?.data;
540
+ if (!data || typeof data !== "object") {
541
+ throw new Error("Could not parse ZCode coding plan quota: missing data");
542
+ }
543
+ const limits = Array.isArray(data.limits) ? data.limits : [];
544
+ const buckets = limits.map(normalizeZcodeQuotaLimit).filter((bucket) => bucket?.window);
545
+ const sorted = buckets.slice().sort((a, b) => {
546
+ const aTotal = a.total_units || 0;
547
+ const bTotal = b.total_units || 0;
548
+ return bTotal - aTotal;
549
+ });
550
+ const level = typeof data.level === "string" && data.level.trim() ? data.level.trim() : null;
551
+ return {
552
+ server_time: null,
553
+ plan_id: level,
554
+ plan_label: level ? deriveZcodePlanLabel(level) || level : "Coding",
555
+ buckets: sorted,
556
+ primary_window: sorted[0]?.window || null,
557
+ secondary_window: sorted[1]?.window || null,
558
+ };
169
559
  }
170
560
 
171
- async function fetchZcodeLimits({ home, env, fetchImpl = fetch } = {}) {
561
+ function parseZcodeLogTimestamp(line) {
562
+ const match = line.match(/^\[(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{3})\]/);
563
+ if (!match) return null;
564
+ const [, year, month, day, hour, minute, second, ms] = match;
565
+ const timestamp = new Date(
566
+ Number(year),
567
+ Number(month) - 1,
568
+ Number(day),
569
+ Number(hour),
570
+ Number(minute),
571
+ Number(second),
572
+ Number(ms),
573
+ );
574
+ const time = timestamp.getTime();
575
+ return Number.isFinite(time) ? time : null;
576
+ }
577
+
578
+ function formatZcodeLogDate(date) {
579
+ const y = date.getFullYear();
580
+ const m = String(date.getMonth() + 1).padStart(2, "0");
581
+ const d = String(date.getDate()).padStart(2, "0");
582
+ return `${y}-${m}-${d}`;
583
+ }
584
+
585
+ function recentZcodeLogPaths({ home, env, nowMs = Date.now() } = {}) {
586
+ const logsDir = path.join(resolveZcodeHome({ home, env }), "v2", "logs");
587
+ const today = new Date(nowMs);
588
+ const yesterday = new Date(nowMs - 24 * 60 * 60 * 1000);
589
+ return [today, yesterday].map((d) => path.join(logsDir, `${formatZcodeLogDate(d)}.log`));
590
+ }
591
+
592
+ function extractZcodeBalanceLogRecord(line) {
593
+ if (!line.includes("[usage-stats] billing/balance 请求完成")) return null;
594
+ const jsonStart = line.indexOf("{");
595
+ if (jsonStart < 0) return null;
596
+ try {
597
+ const entry = JSON.parse(line.slice(jsonStart));
598
+ const body = entry?.payload;
599
+ if (!entry?.success || entry?.code !== 0 || !body || body?.code !== 0) return null;
600
+ if (!Array.isArray(body?.data?.balances) || body.data.balances.length === 0) return null;
601
+ const timestampMs = parseZcodeLogTimestamp(line);
602
+ if (timestampMs == null) return null;
603
+ return {
604
+ body,
605
+ providerKey: typeof entry.providerId === "string" ? entry.providerId : null,
606
+ timestampMs,
607
+ log_timestamp: new Date(timestampMs).toISOString(),
608
+ };
609
+ } catch (_error) {
610
+ return null;
611
+ }
612
+ }
613
+
614
+ function loadLatestZcodeBalanceFromLogs({
615
+ home,
616
+ env = process.env,
617
+ providerKeys = [],
618
+ nowMs = Date.now(),
619
+ requireProviderMatch = false,
620
+ } = {}) {
621
+ if (env.TOKENTRACKER_ZCODE_DISABLE_LOG_FALLBACK === "1") return null;
622
+ const maxAgeMs = parsePositiveInteger(env.TOKENTRACKER_ZCODE_LOG_MAX_AGE_MS, DEFAULT_ZCODE_LOG_FALLBACK_MAX_AGE_MS);
623
+ const preferredProviders = new Set(providerKeys.filter(Boolean));
624
+ let preferred = null;
625
+ let fallback = null;
626
+ for (const logPath of recentZcodeLogPaths({ home, env, nowMs })) {
627
+ if (!fs.existsSync(logPath)) continue;
628
+ let text = "";
629
+ try {
630
+ text = fs.readFileSync(logPath, "utf8");
631
+ } catch (_error) {
632
+ continue;
633
+ }
634
+ const lines = text.split(/\r?\n/);
635
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
636
+ const record = extractZcodeBalanceLogRecord(lines[i]);
637
+ if (!record) continue;
638
+ const ageMs = nowMs - record.timestampMs;
639
+ if (ageMs < 0 || ageMs > maxAgeMs) continue;
640
+ if (!fallback || record.timestampMs > fallback.timestampMs) fallback = record;
641
+ if (preferredProviders.has(record.providerKey) && (!preferred || record.timestampMs > preferred.timestampMs)) {
642
+ preferred = record;
643
+ }
644
+ }
645
+ }
646
+ if (preferred) return preferred;
647
+ return requireProviderMatch && preferredProviders.size > 0 ? null : fallback;
648
+ }
649
+
650
+ function zcodeLogFallbackResult(logRecord, errors = []) {
651
+ if (!logRecord) return null;
652
+ return {
653
+ configured: true,
654
+ error: null,
655
+ source: "zcode-log",
656
+ provider_key: logRecord.providerKey,
657
+ log_timestamp: logRecord.log_timestamp,
658
+ provider_errors: errors,
659
+ ...normalizeZcodeBalanceResponse(logRecord.body),
660
+ };
661
+ }
662
+
663
+ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now() } = {}) {
172
664
  if (!isZcodeInstalled({ home, env })) {
173
665
  return { configured: false };
174
666
  }
175
- const auth = loadZcodeApiKey({ home, env });
176
- if (!auth) {
667
+ const authCandidates = loadZcodeAuthCandidates({ home, env });
668
+ if (!authCandidates.length) {
669
+ const logFallback = loadLatestZcodeBalanceFromLogs({ home, env, nowMs });
670
+ if (logFallback) return zcodeLogFallbackResult(logFallback, []);
177
671
  return { configured: false };
178
672
  }
179
- try {
180
- const body = await fetchZcodeBilling(auth.apiKey, {
181
- fetchImpl,
182
- // Coding-plan baseURL is ".../zcode-plan/anthropic"; strip the trailing
183
- // "/anthropic" (with or without a trailing slash) to reach the billing root.
184
- baseUrl: auth.baseUrl ? auth.baseUrl.replace(/\/anthropic\/?$/, "") : undefined,
185
- env,
186
- });
187
- const apiCode = typeof body?.code === "number" ? body.code : null;
188
- if (apiCode !== null && apiCode !== 0) {
189
- throw new Error(`ZCode billing API error: code=${apiCode} msg=${body?.msg || "unknown"}`);
673
+ const errors = [];
674
+ let emptySuccess = null;
675
+ for (const auth of authCandidates) {
676
+ try {
677
+ const body = auth.planKind === "coding-plan"
678
+ ? await fetchZcodeCodingPlanQuota(auth.apiKey, {
679
+ fetchImpl,
680
+ quotaUrl: auth.quotaUrl,
681
+ })
682
+ : await fetchZcodeBilling(auth.apiKey, {
683
+ fetchImpl,
684
+ baseUrl: auth.billingBaseUrl,
685
+ env,
686
+ home,
687
+ });
688
+ const normalized = auth.planKind === "coding-plan"
689
+ ? normalizeZcodeCodingPlanQuotaResponse(body)
690
+ : normalizeZcodeBalanceResponse(body);
691
+ if (auth.planKind !== "coding-plan") {
692
+ const apiCode = typeof body?.code === "number" ? body.code : null;
693
+ if (apiCode !== null && apiCode !== 0) {
694
+ throw new Error(`ZCode billing API error: code=${apiCode} msg=${body?.msg || "unknown"}`);
695
+ }
696
+ }
697
+ const result = {
698
+ configured: true,
699
+ error: null,
700
+ provider_key: auth.providerKey,
701
+ ...normalized,
702
+ };
703
+ if (Array.isArray(normalized.buckets) && normalized.buckets.length === 0 && authCandidates.length > 1) {
704
+ emptySuccess = emptySuccess || result;
705
+ continue;
706
+ }
707
+ return result;
708
+ } catch (error) {
709
+ errors.push(`${auth.providerKey}: ${error?.message || "Unknown error"}`);
190
710
  }
191
- return {
192
- configured: true,
193
- error: null,
194
- ...normalizeZcodeBalanceResponse(body),
195
- };
196
- } catch (error) {
197
- return {
198
- configured: true,
199
- error: error?.message || "Unknown error",
200
- };
201
711
  }
712
+ if (emptySuccess) return emptySuccess;
713
+ const logFallback = loadLatestZcodeBalanceFromLogs({
714
+ home,
715
+ env,
716
+ providerKeys: authCandidates.map((auth) => auth.providerKey),
717
+ nowMs,
718
+ requireProviderMatch: authCandidates.some((auth) => auth.planKind === "coding-plan"),
719
+ });
720
+ if (logFallback) return zcodeLogFallbackResult(logFallback, errors);
721
+ return {
722
+ configured: true,
723
+ error: errors[0] || "Unknown error",
724
+ provider_errors: errors,
725
+ };
202
726
  }
203
727
 
204
728
  module.exports = {
205
729
  resolveZcodeHome,
206
730
  resolveZcodeBillingBaseUrl,
731
+ resolveZcodeAppVersion,
207
732
  isZcodeInstalled,
733
+ loadZcodeProviderAvailability,
734
+ loadZcodeSelectedPlanProviderKeys,
735
+ loadZcodeCredential,
736
+ loadLatestZcodeBalanceFromLogs,
737
+ resolveZcodeProviderBillingBaseUrl,
738
+ resolveZcodeProviderQuotaUrl,
739
+ loadZcodeAuthCandidates,
208
740
  loadZcodeApiKey,
209
741
  deriveZcodePlanLabel,
210
742
  normalizeZcodeBalanceResponse,
743
+ normalizeZcodeCodingPlanQuotaResponse,
211
744
  fetchZcodeBilling,
745
+ fetchZcodeCodingPlanQuota,
212
746
  fetchZcodeLimits,
213
747
  };
@@ -1 +0,0 @@
1
- import{r as n,b9 as a}from"./main-COdyqkJx.js";import{u as p}from"./DialogTitle-DhnXV44R.js";import{u as c}from"./useScrollLock-BYRX6K6Z.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};