tokentracker-cli 0.57.0 → 0.58.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.
Files changed (55) hide show
  1. package/README.ja.md +9 -6
  2. package/README.ko.md +9 -6
  3. package/README.md +10 -7
  4. package/README.zh-CN.md +9 -6
  5. package/dashboard/dist/assets/{ActivityHeatmap-BzYFi7If.js → ActivityHeatmap--h7iLeSz.js} +1 -1
  6. package/dashboard/dist/assets/{Card-CWlC0TT9.js → Card-KutJI7Kb.js} +1 -1
  7. package/dashboard/dist/assets/DashboardPage-CZS3KR2C.js +19 -0
  8. package/dashboard/dist/assets/{DevicePage-CkLOUytF.js → DevicePage-Bvf6grjO.js} +1 -1
  9. package/dashboard/dist/assets/{DialogTitle-qpnoyCDp.js → DialogTitle-Cd8SHRAb.js} +1 -1
  10. package/dashboard/dist/assets/{FadeIn-rEScv_td.js → FadeIn-D_0IpM7h.js} +1 -1
  11. package/dashboard/dist/assets/{HeaderGithubStar-DNOf-bAX.js → HeaderGithubStar-wllgUDeU.js} +1 -1
  12. package/dashboard/dist/assets/{IpCheckPage-DwsvZICX.js → IpCheckPage-oUeKrhWf.js} +1 -1
  13. package/dashboard/dist/assets/{LandingPage-BzQCcu83.js → LandingPage-BqzdZPJZ.js} +1 -1
  14. package/dashboard/dist/assets/{LeaderboardAvatar-BnXyZa5v.js → LeaderboardAvatar-Bib0pX-r.js} +1 -1
  15. package/dashboard/dist/assets/{LeaderboardPage-W9W6St_R.js → LeaderboardPage-BjLZyZb-.js} +3 -3
  16. package/dashboard/dist/assets/{LeaderboardProfileModal-udtOlLe2.js → LeaderboardProfileModal-GiTmpwyn.js} +1 -1
  17. package/dashboard/dist/assets/{LeaderboardProfilePage-gSEw4uv8.js → LeaderboardProfilePage-Df_jL82E.js} +1 -1
  18. package/dashboard/dist/assets/LimitsPage-BhPNmv6R.js +2 -0
  19. package/dashboard/dist/assets/{LocalOnlyNotice-BZlvc0vN.js → LocalOnlyNotice-B01hqlKr.js} +1 -1
  20. package/dashboard/dist/assets/{LoginPage-B565FVDA.js → LoginPage-BjqX1wZg.js} +1 -1
  21. package/dashboard/dist/assets/{PopoverPopup-BamnkoKk.js → PopoverPopup-CWyN32rM.js} +1 -1
  22. package/dashboard/dist/assets/{ResetPasswordPage-C0-ziJ-w.js → ResetPasswordPage-Bw7tWm1t.js} +1 -1
  23. package/dashboard/dist/assets/{Select-DT22H5j7.js → Select-DbBT6mPJ.js} +1 -1
  24. package/dashboard/dist/assets/{SelectItemText-B6Wn709O.js → SelectItemText-XGWezP4M.js} +1 -1
  25. package/dashboard/dist/assets/{SettingsPage-Dal75OgQ.js → SettingsPage-CrA-jBWs.js} +1 -1
  26. package/dashboard/dist/assets/{SkillsPage-HoRP1WBr.js → SkillsPage-CfYTgQMY.js} +1 -1
  27. package/dashboard/dist/assets/{WidgetsPage-e-mw3Vkq.js → WidgetsPage-DjE_NMiT.js} +1 -1
  28. package/dashboard/dist/assets/{WrappedPage-BBFbsssQ.js → WrappedPage-MciuOQFt.js} +1 -1
  29. package/dashboard/dist/assets/{agent-logos-q4KY3SrS.js → agent-logos-JyXME_ur.js} +1 -1
  30. package/dashboard/dist/assets/{arrow-up-right-BF3pw1i_.js → arrow-up-right-U8aUNwfl.js} +1 -1
  31. package/dashboard/dist/assets/{download-8G10BzKF.js → download-CK8DaHH6.js} +1 -1
  32. package/dashboard/dist/assets/{info-D62z4eaa.js → info-DF9b_Lut.js} +1 -1
  33. package/dashboard/dist/assets/{main-BqomqDjC.js → main-4M5oNV2-.js} +15 -11
  34. package/dashboard/dist/assets/{main-Bb0Bwbp7.css → main-xFJe2FDa.css} +1 -1
  35. package/dashboard/dist/assets/use-limits-display-prefs--1a77fnk.js +1 -0
  36. package/dashboard/dist/assets/{use-native-settings-DJPC20Xw.js → use-native-settings-PZ_s8TF_.js} +1 -1
  37. package/dashboard/dist/assets/{use-usage-limits-Ccr9ZCYy.js → use-usage-limits-B11QqFl7.js} +1 -1
  38. package/dashboard/dist/assets/{useCurrency-DwQhSqB1.js → useCurrency-CzRjastg.js} +1 -1
  39. package/dashboard/dist/assets/{useScrollLock-DZ9md-qB.js → useScrollLock-BXcNDJbM.js} +1 -1
  40. package/dashboard/dist/brand-logos/zcode.svg +1 -0
  41. package/dashboard/dist/index.html +2 -2
  42. package/dashboard/dist/share.html +2 -2
  43. package/package.json +2 -2
  44. package/src/commands/init.js +9 -2
  45. package/src/commands/status.js +11 -0
  46. package/src/commands/sync.js +204 -4
  47. package/src/lib/pricing/curated-overrides.json +2 -0
  48. package/src/lib/pricing/matcher.js +17 -2
  49. package/src/lib/pricing/seed-snapshot.json +1 -1
  50. package/src/lib/rollout.js +54 -54
  51. package/src/lib/usage-limits.js +6 -1
  52. package/src/lib/zcode-limits.js +213 -0
  53. package/dashboard/dist/assets/DashboardPage-CI6QAWEh.js +0 -19
  54. package/dashboard/dist/assets/LimitsPage-Cw4N6JeE.js +0 -2
  55. package/dashboard/dist/assets/use-limits-display-prefs-C0ttQ7qt.js +0 -1
@@ -2520,65 +2520,64 @@ function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
2520
2520
  return out;
2521
2521
  }
2522
2522
 
2523
- // Mimo (mimocode) reuses OpenCode's `message` table schema, but its DB ALSO
2524
- // contains the user's imported Claude Code history: on install, mimocode reads
2525
- // ~/.claude/projects/*.jsonl and copies those sessions into its own DB,
2526
- // recording each in the `claude_import` table (source_path session_id +
2527
- // message_ids). Those imported messages are already counted by the Claude
2528
- // parser, so counting them here would double-count Claude usage AND mislabel it
2529
- // as "mimo".
2523
+ // mimocode mirrors the user's Claude Code + claude-mem history into its own
2524
+ // `message` table via an explicit `claude_import` AND a live observer /
2525
+ // session sync so the overwhelming majority of rows are anthropic-endpoint
2526
+ // turns (providerID="anthropic") the Claude parser ALREADY counts as
2527
+ // source=claude. On the dev's box that's ~3.9B mirrored tokens vs ~22M genuine
2528
+ // mimo tokens. Counting the mirror under "mimo" double-counts and mislabels
2529
+ // Claude usage (user saw claude-* rows under the MIMO provider).
2530
2530
  //
2531
- // `message_ids` was added to claude_import via a migration, so rows written by
2532
- // an older mimocode have it NULL. For those we fall back to excluding the whole
2533
- // imported session by session_id a claude_import.session_id is always an
2534
- // import-created session, never a native mimo one, so this is safe. Rows that
2535
- // DO carry message_ids use precise per-id exclusion, which preserves any native
2536
- // turns the user later added to an imported session.
2537
- function readMimoImportedKeys(dbPath, sqliteOptions = {}) {
2538
- const sql = `SELECT session_id, message_ids FROM claude_import`;
2539
- let rows;
2540
- try {
2541
- rows = readSqliteJsonRows(dbPath, sql, {
2542
- label: "Mimo import",
2543
- maxBuffer: 200 * 1024 * 1024,
2544
- timeout: 30_000,
2545
- ...sqliteOptions,
2546
- });
2547
- } catch (_e) {
2548
- // claude_import table absent (older/newer mimocode) treat as no imports.
2549
- return { ids: new Set(), sessions: new Set() };
2550
- }
2551
- const ids = new Set();
2552
- const sessions = new Set();
2553
- for (const row of rows) {
2554
- if (!row) continue;
2555
- let arr = null;
2556
- if (typeof row.message_ids === "string") {
2557
- try {
2558
- arr = JSON.parse(row.message_ids);
2559
- } catch (_e) {
2560
- arr = null;
2561
- }
2562
- }
2563
- if (Array.isArray(arr) && arr.length > 0) {
2564
- for (const id of arr) if (typeof id === "string") ids.add(id);
2565
- } else if (typeof row.session_id === "string" && row.session_id) {
2566
- sessions.add(row.session_id);
2567
- }
2568
- }
2569
- return { ids, sessions };
2531
+ // The discriminator is providerID, NOT the model id. mimo's own runtime tags
2532
+ // turns providerID="mimo" (its auto router) or "xiaomi". providerID="anthropic"
2533
+ // means the turn went through a Claude-compatible endpoint plain Claude Code,
2534
+ // OR a mimo-named model the user picked IN Claude Code (e.g. model=mimo-v2.5-pro
2535
+ // run inside Claude Code, logged in ~/.claude, counted as source=claude). Keying
2536
+ // off the model id would wrongly re-count that mimo-v2.5-pro. claude_import is
2537
+ // irrelevant it never covers the observer/session mirror; the provider rule
2538
+ // subsumes it.
2539
+ function isMimoNativeMessage(data) {
2540
+ if (!data) return false;
2541
+ const provider = String(data.providerID || "").toLowerCase();
2542
+ return provider === "mimo" || provider === "xiaomi";
2543
+ }
2544
+
2545
+ // Read only genuine mimo assistant messages (mimo's own models), dropping the
2546
+ // mirrored Claude/claude-mem rows. See isMimoNativeMessage for why.
2547
+ function readMimoDbMessages(dbPath, sqliteOptions = {}) {
2548
+ if (!dbPath || !fssync.existsSync(dbPath)) return [];
2549
+ const all = readOpencodeDbMessages(dbPath, sqliteOptions);
2550
+ return all.filter((m) => isMimoNativeMessage(m.data));
2551
+ }
2552
+
2553
+ // ZCode is Z.ai's (Zhipu) coding agent — another OpenCode-fork that stores
2554
+ // assistant messages in the identical `message` table schema
2555
+ // (~/.zcode/cli/db/db.sqlite). Its own agent runs GLM models through Z.ai /
2556
+ // BigModel endpoints, tagged with a providerID like "builtin:zai-start-plan",
2557
+ // "builtin:zai-coding-plan", or "builtin:bigmodel-coding-plan". ZCode can ALSO
2558
+ // orchestrate bundled claude-code / codex / gemini-cli sub-agents; those turns
2559
+ // carry providerID "anthropic"/"openai"/"google" and are already counted by the
2560
+ // standalone Claude/Codex/Gemini parsers. Keep ONLY ZCode-native (Z.ai /
2561
+ // BigModel / Zhipu) rows so we never double-count. Key off providerID, NEVER the
2562
+ // model id — a GLM model the user ran *inside* Claude Code is source=claude, so
2563
+ // matching "glm" on the model would re-count it. Mirrors the Mimo discipline.
2564
+ function isZcodeNativeMessage(data) {
2565
+ if (!data) return false;
2566
+ const provider = String(data.providerID || "").toLowerCase();
2567
+ return (
2568
+ provider.includes("zai") ||
2569
+ provider.includes("bigmodel") ||
2570
+ provider.includes("zcode") ||
2571
+ provider.includes("zhipu")
2572
+ );
2570
2573
  }
2571
2574
 
2572
- // Read only NATIVE mimo assistant messages token-bearing rows that are not
2573
- // part of any claude_import row (by message id, or session id for legacy import
2574
- // rows without message_ids). See readMimoImportedKeys for why.
2575
- function readMimoDbMessages(dbPath, sqliteOptions = {}) {
2575
+ // Read only genuine ZCode assistant messages (its own GLM models via Z.ai /
2576
+ // BigModel), dropping any bundled sub-agent turns. See isZcodeNativeMessage.
2577
+ function readZcodeDbMessages(dbPath, sqliteOptions = {}) {
2576
2578
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
2577
2579
  const all = readOpencodeDbMessages(dbPath, sqliteOptions);
2578
- if (all.length === 0) return all;
2579
- const { ids, sessions } = readMimoImportedKeys(dbPath, sqliteOptions);
2580
- if (ids.size === 0 && sessions.size === 0) return all;
2581
- return all.filter((m) => !ids.has(m.id) && !sessions.has(m.sessionID));
2580
+ return all.filter((m) => isZcodeNativeMessage(m.data));
2582
2581
  }
2583
2582
 
2584
2583
  async function parseOpencodeDbIncremental({
@@ -9095,6 +9094,7 @@ module.exports = {
9095
9094
  listOpencodeMessageFiles,
9096
9095
  readOpencodeDbMessages,
9097
9096
  readMimoDbMessages,
9097
+ readZcodeDbMessages,
9098
9098
  resolveKiroDbPath,
9099
9099
  resolveKiroJsonlPath,
9100
9100
  resolveHermesPath,
@@ -24,6 +24,7 @@ const {
24
24
  fetchCursorUsageSummary,
25
25
  } = require("./cursor-config");
26
26
  const { fetchGrokLimits } = require("./grok-limits");
27
+ const { fetchZcodeLimits } = require("./zcode-limits");
27
28
  const { readSqliteJsonRows } = require("./sqlite-reader");
28
29
 
29
30
  // 2-minute in-memory cache
@@ -2289,7 +2290,7 @@ async function fetchUsageLimitsUncached({
2289
2290
  const freshClaudeCache = claudeToken ? readFreshClaudeLimitsCache({ home, nowMs }) : null;
2290
2291
 
2291
2292
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
2292
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok] = await Promise.all([
2293
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode] = await Promise.all([
2293
2294
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
2294
2295
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
2295
2296
  (value) => ({ status: "fulfilled", value }),
@@ -2318,6 +2319,8 @@ async function fetchUsageLimitsUncached({
2318
2319
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
2319
2320
  withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch }), "Grok Build", providerTimeoutMs)
2320
2321
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
2322
+ withProviderTimeout(fetchZcodeLimits({ home, env, fetchImpl: providerFetch }), "ZCode", providerTimeoutMs)
2323
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
2321
2324
  ]);
2322
2325
 
2323
2326
  let claude;
@@ -2399,6 +2402,7 @@ async function fetchUsageLimitsUncached({
2399
2402
  antigravity: withPlanLabel(antigravity, antigravity.account_plan, "Antigravity"),
2400
2403
  copilot: withPlanLabel(copilot, copilot.plan_name, "Copilot"),
2401
2404
  grok: withPlanLabel(grok, null, "Grok"),
2405
+ zcode: withPlanLabel(zcode, zcode.plan_label, "ZCode"),
2402
2406
  };
2403
2407
 
2404
2408
  cache = { data, fetchedAt: nowMs };
@@ -2430,4 +2434,5 @@ module.exports = {
2430
2434
  decryptCopilotAuthDbToken,
2431
2435
  describeCopilotOtelStatus,
2432
2436
  fetchGrokLimits,
2437
+ fetchZcodeLimits,
2433
2438
  };
@@ -0,0 +1,213 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+
5
+ const DEFAULT_BILLING_BASE_URL = "https://zcode.z.ai/api/v1/zcode-plan";
6
+
7
+ function resolveZcodeHome({ home, env = process.env } = {}) {
8
+ if (typeof env.TOKENTRACKER_ZCODE_HOME === "string" && env.TOKENTRACKER_ZCODE_HOME.trim()) {
9
+ return path.resolve(env.TOKENTRACKER_ZCODE_HOME.trim());
10
+ }
11
+ if (typeof env.ZCODE_HOME === "string" && env.ZCODE_HOME.trim()) {
12
+ return path.resolve(env.ZCODE_HOME.trim());
13
+ }
14
+ return path.join(home || os.homedir(), ".zcode");
15
+ }
16
+
17
+ function resolveZcodeBillingBaseUrl(env = process.env) {
18
+ const explicit =
19
+ typeof env.TOKENTRACKER_ZCODE_BILLING_BASE_URL === "string"
20
+ ? env.TOKENTRACKER_ZCODE_BILLING_BASE_URL.trim()
21
+ : "";
22
+ if (explicit) return explicit.replace(/\/$/, "");
23
+ return DEFAULT_BILLING_BASE_URL;
24
+ }
25
+
26
+ function isZcodeInstalled({ home, env } = {}) {
27
+ const zcodeHome = resolveZcodeHome({ home, env });
28
+ const configPath = path.join(zcodeHome, "v2", "config.json");
29
+ if (fs.existsSync(configPath)) return true;
30
+ const dbPath = path.join(zcodeHome, "cli", "db", "db.sqlite");
31
+ return fs.existsSync(dbPath);
32
+ }
33
+
34
+ function loadZcodeApiKey({ home, env } = {}) {
35
+ const zcodeHome = resolveZcodeHome({ home, env });
36
+ const configPath = path.join(zcodeHome, "v2", "config.json");
37
+ if (!fs.existsSync(configPath)) return null;
38
+ try {
39
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
40
+ if (!config || typeof config !== "object") return null;
41
+ const providers = config.provider || {};
42
+ // Try the active start-plan first, then coding-plan variants
43
+ const candidates = [
44
+ "builtin:bigmodel-start-plan",
45
+ "builtin:zai-start-plan",
46
+ "builtin:bigmodel-coding-plan",
47
+ "builtin:zai-coding-plan",
48
+ ];
49
+ for (const key of candidates) {
50
+ const provider = providers[key];
51
+ if (!provider || typeof provider !== "object") continue;
52
+ if (provider.enabled === false) continue;
53
+ const apiKey = typeof provider?.options?.apiKey === "string" ? provider.options.apiKey.trim() : "";
54
+ if (apiKey) return { apiKey, providerKey: key, baseUrl: provider?.options?.baseURL || null };
55
+ }
56
+ return null;
57
+ } catch (_error) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function zcodeValNumber(value) {
63
+ if (value == null) return null;
64
+ const n = Number(value);
65
+ return Number.isFinite(n) ? n : null;
66
+ }
67
+
68
+ function zcodeTsToIso(value) {
69
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
70
+ return new Date(value * 1000).toISOString();
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function clampPercent(value) {
76
+ if (value === null || value === undefined || value === "") return null;
77
+ const n = Number(value);
78
+ if (!Number.isFinite(n)) return null;
79
+ if (n <= 0) return 0;
80
+ if (n >= 100) return 100;
81
+ return n;
82
+ }
83
+
84
+ function buildWindow({ usedPercent, resetAt }) {
85
+ const pct = clampPercent(usedPercent);
86
+ if (pct === null) return null;
87
+ return {
88
+ used_percent: pct,
89
+ reset_at: typeof resetAt === "string" && resetAt ? resetAt : null,
90
+ };
91
+ }
92
+
93
+ // Z.ai coding-plan ids look like "zcode-v3-start-plan-0615". The raw id reads
94
+ // terribly as a plan label, so extract just the human tier ("Start"/"Lite"/
95
+ // "Pro"/"Max"); fall back to null (→ bare "ZCode") when no known tier matches.
96
+ function deriveZcodePlanLabel(planId) {
97
+ if (typeof planId !== "string" || !planId) return null;
98
+ const m = planId.toLowerCase().match(/\b(lite|start|pro|max|team|enterprise)\b/);
99
+ if (!m) return null;
100
+ return m[1].charAt(0).toUpperCase() + m[1].slice(1);
101
+ }
102
+
103
+ function normalizeZcodeBalanceResponse(body) {
104
+ const data = body?.data;
105
+ if (!data || typeof data !== "object") {
106
+ throw new Error("Could not parse ZCode balance: missing data");
107
+ }
108
+
109
+ const balances = Array.isArray(data.balances) ? data.balances : [];
110
+ if (!balances.length) {
111
+ throw new Error("Could not parse ZCode balance: no balance buckets");
112
+ }
113
+
114
+ const serverTime = zcodeValNumber(data.server_time);
115
+ const buckets = balances.map((b) => {
116
+ const total = zcodeValNumber(b.total_units);
117
+ const used = zcodeValNumber(b.used_units);
118
+ const remaining = zcodeValNumber(b.remaining_units);
119
+ const periodEnd = zcodeValNumber(b.period_end) || zcodeValNumber(b.expires_at);
120
+ const resetAt = zcodeTsToIso(periodEnd);
121
+ const usedPercent =
122
+ total != null && total > 0 && used != null ? (used / total) * 100 : null;
123
+
124
+ return {
125
+ show_name: typeof b.show_name === "string" ? b.show_name : "",
126
+ entitlement_id: typeof b.entitlement_id === "string" ? b.entitlement_id : "",
127
+ total_units: total,
128
+ used_units: used,
129
+ remaining_units: remaining,
130
+ window: buildWindow({ usedPercent, resetAt }),
131
+ };
132
+ });
133
+
134
+ // Primary window: highest-priority bucket (GLM-5.2 typically)
135
+ // Secondary window: second bucket (GLM-5-Turbo typically)
136
+ const sorted = buckets.slice().sort((a, b) => {
137
+ const aTotal = a.total_units || 0;
138
+ const bTotal = b.total_units || 0;
139
+ return bTotal - aTotal;
140
+ });
141
+
142
+ const planId = typeof balances[0]?.plan_id === "string" ? balances[0].plan_id : null;
143
+ return {
144
+ server_time: serverTime,
145
+ plan_id: planId,
146
+ plan_label: deriveZcodePlanLabel(planId),
147
+ buckets: sorted,
148
+ primary_window: sorted[0]?.window || null,
149
+ secondary_window: sorted[1]?.window || null,
150
+ };
151
+ }
152
+
153
+ async function fetchZcodeBilling(apiKey, { fetchImpl = fetch, baseUrl, env } = {}) {
154
+ const root = (baseUrl || resolveZcodeBillingBaseUrl(env)).replace(/\/$/, "");
155
+ const res = await fetchImpl(`${root}/billing/balance`, {
156
+ method: "GET",
157
+ headers: {
158
+ Authorization: `Bearer ${apiKey}`,
159
+ Accept: "application/json",
160
+ },
161
+ });
162
+ if (res.status === 401 || res.status === 403) {
163
+ throw new Error("Not authenticated with ZCode. Run `zcode` in Terminal to log in.");
164
+ }
165
+ if (!res.ok) {
166
+ throw new Error(`ZCode billing API returned ${res.status}`);
167
+ }
168
+ return res.json();
169
+ }
170
+
171
+ async function fetchZcodeLimits({ home, env, fetchImpl = fetch } = {}) {
172
+ if (!isZcodeInstalled({ home, env })) {
173
+ return { configured: false };
174
+ }
175
+ const auth = loadZcodeApiKey({ home, env });
176
+ if (!auth) {
177
+ return { configured: false };
178
+ }
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"}`);
190
+ }
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
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ resolveZcodeHome,
206
+ resolveZcodeBillingBaseUrl,
207
+ isZcodeInstalled,
208
+ loadZcodeApiKey,
209
+ deriveZcodePlanLabel,
210
+ normalizeZcodeBalanceResponse,
211
+ fetchZcodeBilling,
212
+ fetchZcodeLimits,
213
+ };