tokentracker-cli 0.95.2 → 0.96.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 (118) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +1 -1
  3. package/dashboard/dist/assets/{AchievementBadge-DtB1MwFe.js → AchievementBadge-DKyElm0T.js} +1 -1
  4. package/dashboard/dist/assets/{AchievementsPage--vddu0dC.js → AchievementsPage-BvCx3wND.js} +2 -2
  5. package/dashboard/dist/assets/{AchievementsSection-CDl9hsbj.js → AchievementsSection-DN69LzXX.js} +1 -1
  6. package/dashboard/dist/assets/{BadgeDetailModal-C7WsIRy9.js → BadgeDetailModal-DGiKdZfz.js} +1 -1
  7. package/dashboard/dist/assets/{BotAnimated-CmXyzIhX.js → BotAnimated-zN8rH7_K.js} +1 -1
  8. package/dashboard/dist/assets/{Card-SUbC3VNS.js → Card-COWo-ClV.js} +1 -1
  9. package/dashboard/dist/assets/{ClawdAnimated-BdEbs8Me.js → ClawdAnimated-BWsB7Eat.js} +2 -2
  10. package/dashboard/dist/assets/{CommandPalette-D1RLLeHB.js → CommandPalette-BfvIwuoJ.js} +1 -1
  11. package/dashboard/dist/assets/{CommunityStatsModal-5ESGJgZQ.js → CommunityStatsModal-BQWg7R_y.js} +1 -1
  12. package/dashboard/dist/assets/{ConfirmModal-DsCd51b2.js → ConfirmModal-C7ABSI-O.js} +1 -1
  13. package/dashboard/dist/assets/{Controls-BVrLC_QD.js → Controls-vMzEZzej.js} +1 -1
  14. package/dashboard/dist/assets/DashboardPage-QBhbCMZF.js +1 -0
  15. package/dashboard/dist/assets/{DevicePage-zYjj-m5V.js → DevicePage-3ejUBLWM.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-D66yBqkd.js → DialogClose-D_dvtfGV.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-ryGYdQjX.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-C4ia00lc.js → DialogTitle-_ahf0z8b.js} +1 -1
  19. package/dashboard/dist/assets/{FadeIn-DF2c8XaR.js → FadeIn-D7Jcfz1N.js} +1 -1
  20. package/dashboard/dist/assets/{HeaderGithubStar-Cnr3aqcX.js → HeaderGithubStar-D--Rx5BS.js} +1 -1
  21. package/dashboard/dist/assets/{HoverTooltip-DBOpxlH7.js → HoverTooltip-n1W0z3Wr.js} +1 -1
  22. package/dashboard/dist/assets/{IpCheckPage-CUY6KBaV.js → IpCheckPage-D202tl9B.js} +1 -1
  23. package/dashboard/dist/assets/{LandingPage-DKL8hZEq.js → LandingPage-D6yL2EZo.js} +2 -2
  24. package/dashboard/dist/assets/{LeaderboardAvatar-DRYeMNlq.js → LeaderboardAvatar-ChFg2mtA.js} +1 -1
  25. package/dashboard/dist/assets/{LeaderboardPage-DpD9Az3k.js → LeaderboardPage-BXHwGs8r.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProfileModal-Dfg0V13F.js → LeaderboardProfileModal-B5LoOpCk.js} +3 -3
  27. package/dashboard/dist/assets/{LeaderboardProfilePage-DH-jcVDu.js → LeaderboardProfilePage-jsukhRz8.js} +1 -1
  28. package/dashboard/dist/assets/LimitsPage-PvqkOOwp.js +6 -0
  29. package/dashboard/dist/assets/{LocalOnlyNotice-Bj_m0hw7.js → LocalOnlyNotice-Du0lz657.js} +1 -1
  30. package/dashboard/dist/assets/{LoginCard-BVKvtx60.js → LoginCard-BFEX9x7-.js} +1 -1
  31. package/dashboard/dist/assets/{LoginModal-mUGiSIJN.js → LoginModal-DqC7_d35.js} +1 -1
  32. package/dashboard/dist/assets/{LoginPage-BORCuCUt.js → LoginPage-Dzx8N7HE.js} +1 -1
  33. package/dashboard/dist/assets/{PetPage-D1JfEOVA.js → PetPage-CNlWHF4z.js} +1 -1
  34. package/dashboard/dist/assets/{PopoverPopup-DK3et-HG.js → PopoverPopup-D9jadD3j.js} +1 -1
  35. package/dashboard/dist/assets/{ProviderIcon-By8FAjxR.js → ProviderIcon-Caz5PuAm.js} +1 -1
  36. package/dashboard/dist/assets/{ResetPasswordPage-DPa81jqa.js → ResetPasswordPage-x_GR79sR.js} +1 -1
  37. package/dashboard/dist/assets/{SegmentedControl-DLzS-2eZ.js → SegmentedControl-BxDiDV4R.js} +1 -1
  38. package/dashboard/dist/assets/{Select-C05W6XEM.js → Select-B5o-1sEl.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-CcfB3blP.js → SelectItemText-CiA6L7Wk.js} +1 -1
  40. package/dashboard/dist/assets/{ServiceStatusPage-ByqLbHv1.js → ServiceStatusPage-BVQuVdjg.js} +1 -1
  41. package/dashboard/dist/assets/SessionsPage-DE39wNIO.js +2 -0
  42. package/dashboard/dist/assets/SettingsPage-DJrb_fsK.js +2 -0
  43. package/dashboard/dist/assets/{SkillsPage-BuuqQuNU.js → SkillsPage-COk-YQQ5.js} +1 -1
  44. package/dashboard/dist/assets/{TokenGalaxy-DzccxkXW.js → TokenGalaxy-BPM779Wb.js} +1 -1
  45. package/dashboard/dist/assets/{ToolbarRootContext-XUDMp5uy.js → ToolbarRootContext-CozJT6Vs.js} +1 -1
  46. package/dashboard/dist/assets/{TrendMonitor-B-yYabbz.js → TrendMonitor-BkNy-o1D.js} +1 -1
  47. package/dashboard/dist/assets/{WidgetsPage-CwUDjEFo.js → WidgetsPage-Bpg6H-ZB.js} +1 -1
  48. package/dashboard/dist/assets/{WrappedPage-Dr9ow3gH.js → WrappedPage-C6-NM-RO.js} +1 -1
  49. package/dashboard/dist/assets/__vite-browser-external-BGnq8Ebm.js +45 -0
  50. package/dashboard/dist/assets/__vite-browser-external-CvMMIjhi.css +1 -0
  51. package/dashboard/dist/assets/{agent-logos-Cqc_suYk.js → agent-logos-DpbkRqXT.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-up-right-DSsR3Ntr.js → arrow-up-right-BjOtWUVs.js} +1 -1
  53. package/dashboard/dist/assets/{check-Cy35lAKh.js → check-BlxnX5jZ.js} +1 -1
  54. package/dashboard/dist/assets/{composite-BDwOlhhX.js → composite-Dxmivlk4.js} +1 -1
  55. package/dashboard/dist/assets/{copy-4szu5cYh.js → copy-CIsWjLY5.js} +1 -1
  56. package/dashboard/dist/assets/{download-DdGW6Krz.js → download-QDaQbQdd.js} +1 -1
  57. package/dashboard/dist/assets/{external-link-DwSt83cq.js → external-link-DPV4VU3m.js} +1 -1
  58. package/dashboard/dist/assets/{flame-DubukWqK.js → flame-B_X-7VNp.js} +1 -1
  59. package/dashboard/dist/assets/{format-tokens-B70EdQcN.js → format-tokens-DEx2TJj8.js} +1 -1
  60. package/dashboard/dist/assets/{icons-B86TLdAj.js → icons-983RyGSl.js} +1 -1
  61. package/dashboard/dist/assets/{index-B_KMoBeK.js → index-BONYA8HZ.js} +1 -1
  62. package/dashboard/dist/assets/{index-DI-W-pun.js → index-Cjs5stC0.js} +1 -1
  63. package/dashboard/dist/assets/{index-DzEMcUwI.js → index-qtonw_x4.js} +1 -1
  64. package/dashboard/dist/assets/{info-BzFpLbl6.js → info-DpVOhXZI.js} +1 -1
  65. package/dashboard/dist/assets/limits-providers-BUoquSA-.js +1 -0
  66. package/dashboard/dist/assets/{link-2-BQp7-3T7.js → link-2-CBro-upz.js} +1 -1
  67. package/dashboard/dist/assets/{loader-circle-lrDmbIg5.js → loader-circle-wYFdSyOd.js} +1 -1
  68. package/dashboard/dist/assets/{maximize-2-Cnd_Oth8.js → maximize-2-DhTyQ4ly.js} +1 -1
  69. package/dashboard/dist/assets/{modifiers.esm-CPTpZdHU.js → modifiers.esm-CPljwVhN.js} +1 -1
  70. package/dashboard/dist/assets/{monitor-smartphone-C7UY1zRx.js → monitor-smartphone-D_wr-HOR.js} +1 -1
  71. package/dashboard/dist/assets/{plus-Bglp9N_7.js → plus-BNjfkBEF.js} +1 -1
  72. package/dashboard/dist/assets/{provider-display-CGsFySs6.js → provider-display-BOBtUyFi.js} +1 -1
  73. package/dashboard/dist/assets/{react-DKXriBSF.js → react-yHv9MTUA.js} +1 -1
  74. package/dashboard/dist/assets/{search-D_LLDRdU.js → search-Dfak92ZA.js} +1 -1
  75. package/dashboard/dist/assets/{skills-api-CJWjKIco.js → skills-api-By2PdNa1.js} +1 -1
  76. package/dashboard/dist/assets/{sortable.esm-zlY09tye.js → sortable.esm-B9xy66_v.js} +1 -1
  77. package/dashboard/dist/assets/{store-8e16Ac2h.js → store-oQZR4d9M.js} +1 -1
  78. package/dashboard/dist/assets/{terminal-CR228ekj.js → terminal-pbXWBmZM.js} +1 -1
  79. package/dashboard/dist/assets/{trash-2-BjLWewOp.js → trash-2-CboDPwbW.js} +1 -1
  80. package/dashboard/dist/assets/{use-community-stats-DhOJPaVg.js → use-community-stats-D6HTdnHq.js} +1 -1
  81. package/dashboard/dist/assets/{use-latest-request-guard-lcGhb-NC.js → use-latest-request-guard-hwSr9Cam.js} +1 -1
  82. package/dashboard/dist/assets/{use-limits-display-prefs-DJIImtLN.js → use-limits-display-prefs-B-pbwUS8.js} +1 -1
  83. package/dashboard/dist/assets/{use-ordered-list-BJZfZuoJ.js → use-ordered-list-OtfOqrNb.js} +1 -1
  84. package/dashboard/dist/assets/use-reduced-motion-DGXcZBX3.js +1 -0
  85. package/dashboard/dist/assets/{use-session-efficiency-pref-C-l4b1cD.js → use-session-efficiency-pref-BScvtS8F.js} +1 -1
  86. package/dashboard/dist/assets/{use-transform-UjIt477N.js → use-transform-Cy1ykjEs.js} +1 -1
  87. package/dashboard/dist/assets/{use-usage-limits-DqoTZrqT.js → use-usage-limits-CSMb2zYW.js} +1 -1
  88. package/dashboard/dist/assets/{useCurrency-CnM8aB4v.js → useCurrency-VqF3cy1C.js} +1 -1
  89. package/dashboard/dist/assets/{useTokenFormat-D4pAK2ld.js → useTokenFormat-Cgub4MXu.js} +1 -1
  90. package/dashboard/dist/assets/{zap-Du1IyEtj.js → zap-DStzvQJI.js} +1 -1
  91. package/dashboard/dist/index.html +2 -2
  92. package/dashboard/dist/ip-check.html +2 -2
  93. package/dashboard/dist/leaderboard.html +2 -2
  94. package/dashboard/dist/share.html +2 -2
  95. package/package.json +1 -1
  96. package/src/commands/status.js +4 -4
  97. package/src/commands/sync.js +5 -3
  98. package/src/lib/ark-agent-plan-limits.js +438 -0
  99. package/src/lib/ark-coding-plan-limits.js +103 -28
  100. package/src/lib/codex-model-attribution.js +128 -0
  101. package/src/lib/codex-rollout-parser.js +98 -4
  102. package/src/lib/command-runner.js +13 -0
  103. package/src/lib/local-api.js +3 -0
  104. package/src/lib/pricing/curated-overrides.json +6 -6
  105. package/src/lib/pricing/index.js +38 -1
  106. package/src/lib/pricing/seed-snapshot.json +1 -1
  107. package/src/lib/rollout.js +67 -14
  108. package/src/lib/session-analytics.js +374 -110
  109. package/src/lib/usage-limits.js +130 -18
  110. package/dashboard/dist/assets/DashboardPage-DtUOG6og.js +0 -1
  111. package/dashboard/dist/assets/DialogDescription-BVlaoajd.js +0 -1
  112. package/dashboard/dist/assets/LimitsPage-CVCKPScf.js +0 -5
  113. package/dashboard/dist/assets/SessionsPage-CEDv1thj.js +0 -2
  114. package/dashboard/dist/assets/SettingsPage-DAcndJZQ.js +0 -2
  115. package/dashboard/dist/assets/__vite-browser-external-CCMdjCX2.css +0 -1
  116. package/dashboard/dist/assets/__vite-browser-external-DPOlUkcY.js +0 -45
  117. package/dashboard/dist/assets/limits-providers-DxZOjiXQ.js +0 -1
  118. package/dashboard/dist/assets/use-reduced-motion-DdpxZQoh.js +0 -1
@@ -0,0 +1,438 @@
1
+ "use strict";
2
+
3
+ // Ark Agent Plan (火山方舟 Agent Plan) quota monitoring.
4
+ //
5
+ // Agent Plan (Small/Medium/Large/Max) and Coding Plan (Lite/Pro) are
6
+ // two parallel subscription products that each refresh on three windows —
7
+ // 5-hour (5h for Agent Plan, session for Coding Plan), weekly and monthly —
8
+ // and are shared by compatible coding tools (Claude Code, Codex CLI,
9
+ // OpenCode, TRAE, ...). Official docs present them as parallel, not
10
+ // successor/replacement. TokenTracker already counts those tools' token
11
+ // consumption from their local files, so this module deliberately adds
12
+ // NO consumption source. It only surfaces the subscription quota
13
+ // percentage, which is otherwise only visible in the Volcano console web
14
+ // page. Coding Plan is handled by a separate provider module
15
+ // `ark-coding-plan-limits.js` (dual Provider, issue #555).
16
+ //
17
+ // The quota is read through the user's own `arkcli` binary
18
+ // (`arkcli usage plan --format json`), which is already installed and logged
19
+ // in for users of the Ark CLI ecosystem. Feature-detected: when `arkcli` is
20
+ // missing, the provider simply reports `configured: false` and stays out of
21
+ // the way. Mirrors ark-coding-plan-limits.js; shares the command runner with
22
+ // usage-limits.js through ./command-runner (no circular dependency).
23
+
24
+ const fs = require("node:fs");
25
+ const os = require("node:os");
26
+ const path = require("node:path");
27
+
28
+ const { runCommand, resolveBinaryPath, statBinaryInDirs, commonGlobalBinDirectories, resolvedCliEnvironment } = require("./command-runner");
29
+
30
+ const ARK_LIMITS_CACHE_FILE = "ark-agent-plan-limits-cache.json";
31
+ const ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
32
+ const ARK_USAGE_PLAN_TIMEOUT_MS = 10_000;
33
+ const ARK_PROFILE_SHOW_TIMEOUT_MS = 2_500;
34
+ const ARK_CLI_STDERR_TRIM = 400;
35
+ const ARK_PROVIDER_TIMEOUT_MS = 15_000;
36
+ const ARK_PROVIDER_BUDGET_GUARD_MS = 1_500;
37
+
38
+ // arkcli period label -> canonical window slot.
39
+ // Agent Plan uses `5h` for the 5-hour rolling window; `daily` is a
40
+ // visual-only quota not shown in the 3-window panel.
41
+ const ARK_PERIOD_WINDOW = {
42
+ "5h": "primary_window",
43
+ session: "primary_window",
44
+ weekly: "secondary_window",
45
+ monthly: "tertiary_window",
46
+ };
47
+
48
+ // Agent Plan personal product. Team products (agent-plan-team) are
49
+ // seat-scoped and not surfaced in this personal-quota panel.
50
+ const ARK_PERSONAL_PRODUCTS = ["agent-plan"];
51
+ const ARK_PRODUCT_PRIORITY = ["agent-plan"];
52
+
53
+ function clampPercent(value) {
54
+ return Math.max(0, Math.min(100, value));
55
+ }
56
+
57
+ function normalizeResetAt(value) {
58
+ if (value === null || value === undefined || value === "") return null;
59
+ if (typeof value === "number" || /^\d+(?:\.\d+)?$/.test(String(value).trim())) {
60
+ const raw = Number(value);
61
+ if (!Number.isFinite(raw)) return null;
62
+ const milliseconds = raw > 10_000_000_000 ? raw : raw * 1000;
63
+ const date = new Date(milliseconds);
64
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
65
+ }
66
+ const timestamp = Date.parse(String(value));
67
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
68
+ }
69
+
70
+ function planLabelForTier(tier) {
71
+ const normalized = String(tier || "").trim().toLowerCase();
72
+ const mapping = {
73
+ pro: "Pro",
74
+ lite: "Lite",
75
+ small: "Small",
76
+ medium: "Medium",
77
+ large: "Large",
78
+ max: "Max",
79
+ };
80
+ if (mapping[normalized]) return mapping[normalized];
81
+ return tier && String(tier).trim() ? String(tier).trim() : null;
82
+ }
83
+
84
+ /**
85
+ * Normalize the JSON payload returned by `arkcli plans get --format json`.
86
+ * Returns null when there is no Agent Plan entry.
87
+ */
88
+ function normalizeArkAgentPlansResponse(body) {
89
+ if (!body || typeof body !== "object") return null;
90
+ const plans = Array.isArray(body.plans) ? body.plans : [];
91
+ for (const key of ARK_PRODUCT_PRIORITY) {
92
+ const plan = plans.find((entry) => entry?.key === key);
93
+ if (plan?.tier) return String(plan.tier);
94
+ }
95
+ return null;
96
+ }
97
+ // Backwards-compat alias used by older callers
98
+ const normalizeArkPlansResponse = normalizeArkAgentPlansResponse;
99
+
100
+ function arkProfileIdentity(body) {
101
+ const profile = body?.profile && typeof body.profile === "object" ? body.profile : body;
102
+ const name = typeof body?.profile === "string"
103
+ ? body.profile
104
+ : profile?.name || profile?.profile || profile?.profile_name;
105
+ let userId = profile?.user_id || profile?.userId || body?.user_id || body?.userId;
106
+ if (!userId) {
107
+ const trnMatch = String(profile?.owner_trn || "").match(/::(\d+):/);
108
+ if (trnMatch) userId = trnMatch[1];
109
+ else {
110
+ const keyMatch = String(profile?.identity_key || "").match(/-(\d+)$/);
111
+ if (keyMatch) userId = keyMatch[1];
112
+ }
113
+ }
114
+ const identity = [name, userId].filter(Boolean).join(":");
115
+ return identity || null;
116
+ }
117
+
118
+ function resolveArkProductItem(items) {
119
+ for (const product of ARK_PRODUCT_PRIORITY) {
120
+ const item = items.find((entry) => entry?.product === product && entry?.subscribed === true);
121
+ if (item) return item;
122
+ }
123
+ return null;
124
+ }
125
+
126
+ function percentFromPeriod(period) {
127
+ // null / undefined / blank percent must fall through to the used/total
128
+ // fallback: Number(null) and Number("") both coerce to 0, which would
129
+ // otherwise report a fake fresh 0% window instead of deriving real usage
130
+ // (the Agent Plan 5h period can omit percent entirely, #555).
131
+ const rawPercent = period?.percent;
132
+ if (rawPercent !== null && rawPercent !== undefined && String(rawPercent).trim() !== "") {
133
+ const direct = Number(rawPercent);
134
+ if (Number.isFinite(direct)) return direct;
135
+ }
136
+ const used = Number(period?.used);
137
+ const total = Number(period?.total);
138
+ if (Number.isFinite(used) && Number.isFinite(total) && total > 0) {
139
+ return (used / total) * 100;
140
+ }
141
+ return NaN;
142
+ }
143
+
144
+ /**
145
+ * Normalize the JSON payload returned by `arkcli usage plan --format json`.
146
+ * Returns `null` when the account has no active Agent Plan subscription
147
+ * (caller reports `configured: false`). Throws when the payload shape is
148
+ * unusable so the caller can fall back to the disk cache.
149
+ */
150
+ function normalizeArkAgentPlanResponse(body) {
151
+ if (!body || typeof body !== "object") {
152
+ throw new Error("Ark Agent Plan response is not an object.");
153
+ }
154
+ const items = Array.isArray(body.items) ? body.items : [];
155
+ const item = resolveArkProductItem(items);
156
+ if (!item) return null;
157
+
158
+ const windows = {};
159
+ const periods = Array.isArray(item.periods) ? item.periods : [];
160
+ for (const period of periods) {
161
+ if (period?.label === "daily") continue;
162
+ const slot = ARK_PERIOD_WINDOW[period?.label];
163
+ if (!slot) continue;
164
+ const percent = percentFromPeriod(period);
165
+ if (!Number.isFinite(percent)) continue;
166
+ windows[slot] = {
167
+ used_percent: clampPercent(percent),
168
+ reset_at: normalizeResetAt(period.reset_at),
169
+ unit: "calls",
170
+ };
171
+ }
172
+ if (!windows.primary_window && !windows.secondary_window && !windows.tertiary_window) {
173
+ throw new Error("Ark Agent Plan response contains no usable quota periods.");
174
+ }
175
+
176
+ return {
177
+ configured: true,
178
+ error: null,
179
+ plan_label: planLabelForTier(item.tier),
180
+ product: item.product || null,
181
+ primary_window: windows.primary_window || null,
182
+ secondary_window: windows.secondary_window || null,
183
+ tertiary_window: windows.tertiary_window || null,
184
+ source: "provider-api",
185
+ profile_identity: arkProfileIdentity(body.viewer),
186
+ };
187
+ }
188
+ // Alias for shared shape
189
+ const normalizeArkCodingPlanResponse = normalizeArkAgentPlanResponse;
190
+
191
+ function arkAgentPlanCachePath({ home = os.homedir() } = {}) {
192
+ return path.join(home, ".tokentracker", "tracker", ARK_LIMITS_CACHE_FILE);
193
+ }
194
+
195
+ function readArkAgentPlanLimitsCache({ home = os.homedir(), nowMs = Date.now(), profileIdentity } = {}) {
196
+ try {
197
+ const parsed = JSON.parse(fs.readFileSync(arkAgentPlanCachePath({ home }), "utf8"));
198
+ if (profileIdentity && parsed?.profile_identity !== profileIdentity) return null;
199
+ const cachedAtMs = Date.parse(parsed?.cached_at || "");
200
+ if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
201
+ const windows = [parsed?.primary_window, parsed?.secondary_window, parsed?.tertiary_window];
202
+ const surviving = windows.map((window) => {
203
+ if (!window) return null;
204
+ const resetAtMs = Date.parse(window.reset_at || "");
205
+ if (Number.isFinite(resetAtMs) && resetAtMs <= nowMs) return null;
206
+ return window;
207
+ });
208
+ if (surviving.every((window) => !window)) return null;
209
+ const bounded = surviving.map((window) => {
210
+ if (!window) return null;
211
+ return Number.isFinite(Date.parse(window.reset_at || ""))
212
+ || nowMs - cachedAtMs <= ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS
213
+ ? window
214
+ : null;
215
+ });
216
+ if (bounded.every((window) => !window)) return null;
217
+ return {
218
+ configured: true,
219
+ error: null,
220
+ plan_label: typeof parsed?.plan_label === "string" ? parsed.plan_label : null,
221
+ product: typeof parsed?.product === "string" ? parsed.product : null,
222
+ primary_window: bounded[0],
223
+ secondary_window: bounded[1],
224
+ tertiary_window: bounded[2],
225
+ cached: true,
226
+ stale: true,
227
+ cached_at: parsed.cached_at,
228
+ source: "disk-cache",
229
+ };
230
+ } catch (_error) {
231
+ return null;
232
+ }
233
+ }
234
+
235
+ function writeArkAgentPlanLimitsCache(limits, { home = os.homedir(), nowMs = Date.now() } = {}) {
236
+ if (!limits?.configured || limits.error) return;
237
+ const cachePath = arkAgentPlanCachePath({ home });
238
+ const payload = {
239
+ plan_label: limits.plan_label || null,
240
+ product: limits.product || null,
241
+ profile_identity: limits.profile_identity || null,
242
+ primary_window: limits.primary_window || null,
243
+ secondary_window: limits.secondary_window || null,
244
+ tertiary_window: limits.tertiary_window || null,
245
+ cached_at: new Date(nowMs).toISOString(),
246
+ };
247
+ try {
248
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
249
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
250
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
251
+ fs.renameSync(tmpPath, cachePath);
252
+ } catch (_error) {}
253
+ }
254
+
255
+ function hasArkCliInstallEvidence({ home = os.homedir(), platform = process.platform } = {}) {
256
+ const candidates = [
257
+ path.join(home, ".arkcli"),
258
+ path.join(home, ".config", "arkcli"),
259
+ ];
260
+ if (platform === "win32") {
261
+ candidates.push(path.join(home, "AppData", "Roaming", "arkcli"));
262
+ }
263
+ for (const dir of candidates) {
264
+ try {
265
+ if (fs.statSync(dir).isDirectory()) return true;
266
+ } catch (_error) {}
267
+ }
268
+ return false;
269
+ }
270
+
271
+ function trimStderr(stderr) {
272
+ const text = String(stderr || "").trim();
273
+ if (!text) return null;
274
+ return text.length > ARK_CLI_STDERR_TRIM
275
+ ? `${text.slice(0, ARK_CLI_STDERR_TRIM)}…`
276
+ : text;
277
+ }
278
+
279
+ /**
280
+ * Fetch Ark Agent Plan quota windows from the local `arkcli` binary.
281
+ */
282
+ async function fetchArkAgentPlanLimits({
283
+ commandRunner,
284
+ home = os.homedir(),
285
+ nowMs = Date.now(),
286
+ platform = process.platform,
287
+ signal,
288
+ globalBinDirs,
289
+ providerTimeoutMs = ARK_PROVIDER_TIMEOUT_MS,
290
+ } = {}) {
291
+ const startedAtMs = performance.now();
292
+ const budgetedTimeoutMs = (fullTimeoutMs) => {
293
+ if (!Number.isFinite(providerTimeoutMs) || providerTimeoutMs <= 0) return fullTimeoutMs;
294
+ const remainingMs = providerTimeoutMs - (performance.now() - startedAtMs);
295
+ if (remainingMs <= 0) return 0;
296
+ const guardedMs = Math.floor(remainingMs - ARK_PROVIDER_BUDGET_GUARD_MS);
297
+ if (guardedMs <= 0) return 0;
298
+ return Math.min(fullTimeoutMs, guardedMs);
299
+ };
300
+
301
+ const searchDirs = () => Array.isArray(globalBinDirs)
302
+ ? globalBinDirs
303
+ : commonGlobalBinDirectories({ home, platform });
304
+
305
+ let arkcliPath;
306
+ if (hasArkCliInstallEvidence({ home, platform })) {
307
+ try {
308
+ arkcliPath = await resolveBinaryPath("arkcli", { commandRunner, home, platform, signal, globalBinDirs });
309
+ } catch (_error) {
310
+ arkcliPath = null;
311
+ }
312
+ } else {
313
+ arkcliPath = statBinaryInDirs("arkcli", searchDirs(), platform);
314
+ }
315
+ if (!arkcliPath) return { configured: false };
316
+
317
+ const commandOptions = {
318
+ env: resolvedCliEnvironment(arkcliPath, { platform }),
319
+ signal,
320
+ killProcessGroup: true,
321
+ platform,
322
+ useShell: platform === "win32",
323
+ };
324
+
325
+ const usageTimeoutMs = budgetedTimeoutMs(ARK_USAGE_PLAN_TIMEOUT_MS);
326
+ if (usageTimeoutMs <= 0) {
327
+ return { configured: true, error: "Ark Agent Plan provider timed out before arkcli could run." };
328
+ }
329
+
330
+ const result = await runCommand(
331
+ commandRunner,
332
+ arkcliPath,
333
+ ["usage", "plan", "--format", "json"],
334
+ { ...commandOptions, timeout: usageTimeoutMs },
335
+ );
336
+
337
+ const failWithCache = async (message) => {
338
+ const profileTimeoutMs = budgetedTimeoutMs(ARK_PROFILE_SHOW_TIMEOUT_MS);
339
+ const profileIdentity = profileTimeoutMs > 0
340
+ ? await runCommand(
341
+ commandRunner,
342
+ arkcliPath,
343
+ ["profile", "show", "--format", "json"],
344
+ { ...commandOptions, timeout: profileTimeoutMs },
345
+ ).then((profileResult) => {
346
+ if (profileResult?.error || profileResult?.status !== 0) return null;
347
+ try {
348
+ return arkProfileIdentity(JSON.parse(String(profileResult.stdout || "")));
349
+ } catch (_error) {
350
+ return null;
351
+ }
352
+ }).catch(() => null)
353
+ : null;
354
+ const cached = readArkAgentPlanLimitsCache({ home, nowMs, profileIdentity });
355
+ if (cached) return cached;
356
+ return { configured: true, error: message };
357
+ };
358
+
359
+ if (result?.error || result?.status !== 0) {
360
+ const isExit127 = result?.status === 127;
361
+ const baseDetail = result?.error?.message
362
+ || (result?.status !== 0 && result?.status !== null
363
+ ? `arkcli exited with code ${result.status}`
364
+ : "arkcli usage plan failed");
365
+ const detail = isExit127
366
+ ? "arkcli exited with code 127 — check its runtime/PATH or update arkcli: npm i -g @volcengine/ark-cli"
367
+ : baseDetail;
368
+ const stderr = trimStderr(result?.stderr);
369
+ const message = stderr ? `${detail}: ${stderr}` : detail;
370
+ return failWithCache(message);
371
+ }
372
+
373
+ let body;
374
+ try {
375
+ body = JSON.parse(String(result?.stdout || ""));
376
+ } catch (_error) {
377
+ return failWithCache("arkcli usage plan returned invalid JSON.");
378
+ }
379
+
380
+ let limits;
381
+ try {
382
+ limits = normalizeArkAgentPlanResponse(body);
383
+ } catch (error) {
384
+ return failWithCache(error?.message || "Ark Agent Plan response could not be parsed.");
385
+ }
386
+ if (!limits) {
387
+ const entry = Array.isArray(body?.items)
388
+ ? body.items.find(
389
+ (candidate) =>
390
+ ARK_PERSONAL_PRODUCTS.includes(candidate?.product) && candidate?.subscribed === false,
391
+ )
392
+ : null;
393
+ if (entry) {
394
+ try {
395
+ fs.unlinkSync(arkAgentPlanCachePath({ home }));
396
+ } catch (_error) {}
397
+ }
398
+ return { configured: false };
399
+ }
400
+
401
+ if (!limits.plan_label) {
402
+ const plansTimeoutMs = budgetedTimeoutMs(ARK_USAGE_PLAN_TIMEOUT_MS);
403
+ if (plansTimeoutMs > 0) {
404
+ const plansResult = await runCommand(
405
+ commandRunner,
406
+ arkcliPath,
407
+ ["plans", "get", "--format", "json"],
408
+ { ...commandOptions, timeout: plansTimeoutMs },
409
+ );
410
+ if (!plansResult?.error && plansResult?.status === 0) {
411
+ try {
412
+ const tier = normalizeArkAgentPlansResponse(JSON.parse(String(plansResult.stdout || "")));
413
+ if (tier) limits.plan_label = planLabelForTier(tier);
414
+ } catch (_error) {}
415
+ }
416
+ }
417
+ }
418
+
419
+ writeArkAgentPlanLimitsCache(limits, { home, nowMs });
420
+ return limits;
421
+ }
422
+
423
+ module.exports = {
424
+ ARK_PERIOD_WINDOW,
425
+ ARK_PERSONAL_PRODUCTS,
426
+ ARK_PRODUCT_PRIORITY,
427
+ normalizeArkAgentPlansResponse,
428
+ normalizeArkPlansResponse,
429
+ arkProfileIdentity,
430
+ normalizeArkAgentPlanResponse,
431
+ normalizeArkCodingPlanResponse,
432
+ readArkAgentPlanLimitsCache,
433
+ writeArkAgentPlanLimitsCache,
434
+ hasArkCliInstallEvidence,
435
+ fetchArkAgentPlanLimits,
436
+ resolveArkProductItem,
437
+ percentFromPeriod,
438
+ };