tokentracker-cli 0.95.1 → 0.96.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/dashboard/dist/assets/{AchievementBadge-Cyju9OnL.js → AchievementBadge-Bb2ljquW.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-Bh2dQR-I.js → AchievementsPage-ByUP2x8a.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DSFAM0Zp.js → AchievementsSection-_L6sCLTw.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CgVF0VI0.js → BadgeDetailModal-B9n_-45B.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-DI-rTxI9.js → BotAnimated-lWmg-aZg.js} +1 -1
- package/dashboard/dist/assets/{Card-DAlV-PSI.js → Card-aSp259T0.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-oOYKgcpq.js → ClawdAnimated-Cy-XaTXW.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-bYE5uD63.js → CommandPalette-CNwtKCUm.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DCx0sQjk.js → CommunityStatsModal-DJSezaPV.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DrYf9m15.js → ConfirmModal-CVjmhZd6.js} +1 -1
- package/dashboard/dist/assets/{Controls-DAift9Z4.js → Controls-BIj6yVxE.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CLswnOil.js → DashboardPage-BjYZLmqQ.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-uWLgMC4Z.js → DevicePage-ptsI2fZc.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-NitVrdVC.js → DialogClose-D1pLoZ56.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-KY7X94CG.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CA_ZstJL.js → DialogTitle-DfSBUjWf.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CIkYDamA.js → FadeIn-DBn4L_BV.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CYTJNLpL.js → HeaderGithubStar-EJLBwEqp.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-jh3sI8NC.js → HoverTooltip-DaSrZhYd.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Brpe-N7B.js → IpCheckPage-diQTVaYt.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-D4PjaNcN.js → LandingPage-BqCxjwar.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-D9dc6frq.js → LeaderboardAvatar-CQImCsPN.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CEuqopiC.js → LeaderboardPage-D2ZDUi_O.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-TeYdXeIA.js → LeaderboardProfileModal-DHvADX1d.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DmtuwJkL.js → LeaderboardProfilePage-CJsedB28.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-CAwff24b.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-V8_KFKOD.js → LocalOnlyNotice-Cmnvu9tI.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-ud0lCY_u.js → LoginCard-BQmKkD7X.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-Bnemx4UD.js → LoginModal-2oPROePZ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-v89BHssa.js → LoginPage-aqdUJoQK.js} +1 -1
- package/dashboard/dist/assets/{PetPage-Bg9kEi7o.js → PetPage-Bn3p_o5r.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CCHVwh0w.js → PopoverPopup-DXD_YdiB.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BecpRylD.js → ProviderIcon-BZZ45-gY.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CsUtFeUr.js → ResetPasswordPage-Bn5ITiHQ.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-3rBqbzW_.js → SegmentedControl-hGXboQZy.js} +1 -1
- package/dashboard/dist/assets/{Select-DZ2ll7Qv.js → Select-BP8aVRZ1.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Dr1W47W6.js → SelectItemText-Odx47kND.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-Cc1LXdAh.js → ServiceStatusPage-DUueam0N.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-BA9IFqQ1.js +2 -0
- package/dashboard/dist/assets/{SettingsPage-2wYQ2ly6.js → SettingsPage-DLOHHB4f.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-BEDAwBTN.js → SkillsPage-Ds2vH_PG.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DxG0dBeG.js → TokenGalaxy-CWBR4jnB.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DG1pr5UP.js → ToolbarRootContext-Bmg56vYz.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CeuEQ0XC.js → TrendMonitor-BQzcllvg.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-ALDlZ5aR.js → WidgetsPage-Bu3tHDjO.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-BF59ouR-.js → WrappedPage-BrAMJhgy.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DXtgoDkV.js → __vite-browser-external-4IGm4Djn.js} +12 -12
- package/dashboard/dist/assets/{agent-logos-D6Ya1qw1.js → agent-logos-B8lT-HcI.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DUR69elJ.js → arrow-up-right-rpBCOTg2.js} +1 -1
- package/dashboard/dist/assets/{check-9_1xp5h5.js → check-DIKw7TYH.js} +1 -1
- package/dashboard/dist/assets/{composite-CEzovOpb.js → composite-_NeXO0lv.js} +1 -1
- package/dashboard/dist/assets/{copy-DEoLQqJE.js → copy-CsfAtL7P.js} +1 -1
- package/dashboard/dist/assets/{download-BNjfcJ1g.js → download-DM1W9pIW.js} +1 -1
- package/dashboard/dist/assets/{external-link-fV_WJ5f_.js → external-link-CwbuMbeQ.js} +1 -1
- package/dashboard/dist/assets/{flame-DqpuFJ14.js → flame-DcYbLdhE.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CatddjmK.js → format-tokens-CEvr3dNy.js} +1 -1
- package/dashboard/dist/assets/{icons-CBT1KI7V.js → icons-DaGMVsJ5.js} +1 -1
- package/dashboard/dist/assets/{index-_2LEUZTl.js → index-BVVLPj3N.js} +1 -1
- package/dashboard/dist/assets/{index-BK3SzL7v.js → index-ChZkJjpk.js} +1 -1
- package/dashboard/dist/assets/{index-tK7s2SeO.js → index-T7OjmEuX.js} +1 -1
- package/dashboard/dist/assets/{info-DBotcfT9.js → info-CAjN3dX4.js} +1 -1
- package/dashboard/dist/assets/limits-providers-jn7Yln_V.js +1 -0
- package/dashboard/dist/assets/{link-2-Wc54x2fh.js → link-2-DaWH2Tyc.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-nRhGIddE.js → loader-circle-CnF8qPi1.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Dw3_GuKW.js → maximize-2-B-2Pnj6g.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-rTZ5BUxp.js → modifiers.esm-BoDOvUKl.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-CNVAqV_N.js → monitor-smartphone-DDgwxG_E.js} +1 -1
- package/dashboard/dist/assets/{plus-B5oPOXhp.js → plus-DIc1YUWB.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DhAUWfC6.js → provider-display-yeNsVCO_.js} +1 -1
- package/dashboard/dist/assets/{react-BKACWrsB.js → react-DZqS6YTX.js} +1 -1
- package/dashboard/dist/assets/{search-DgddmV09.js → search-D6tEppYJ.js} +1 -1
- package/dashboard/dist/assets/{skills-api-BT1uiqeq.js → skills-api-vsA1rc1Z.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-UCoNfZ6z.js → sortable.esm-DDCnS1HI.js} +1 -1
- package/dashboard/dist/assets/{store-ZswY8Voz.js → store-Crne4DqF.js} +1 -1
- package/dashboard/dist/assets/{terminal-BtQyotJS.js → terminal-Ca7dkLMF.js} +1 -1
- package/dashboard/dist/assets/{trash-2-Dc_NLbWs.js → trash-2-CjEAS5ic.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-yOKag_ou.js → use-community-stats-BHx2YxyK.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-BEiszvlI.js → use-latest-request-guard-BLGbXQdf.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-C-l_Mxvy.js → use-limits-display-prefs-DBkAp6ob.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CKjRaO6K.js → use-ordered-list-BTfW5M9u.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-D_kUnDs4.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DPI_--W2.js → use-session-efficiency-pref-DBmd-MqH.js} +1 -1
- package/dashboard/dist/assets/{use-transform-BQUem5OA.js → use-transform-Bp5XrwaQ.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-lvkMBnNR.js → use-usage-limits-BmqRcyhH.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-C5xhS1QV.js → useCurrency-B0YsRDj-.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-FBMikfEq.js → useTokenFormat-PP3D0Ket.js} +1 -1
- package/dashboard/dist/assets/{zap-BsCNYelz.js → zap-BVOm1aBH.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/lib/ark-agent-plan-limits.js +437 -0
- package/src/lib/ark-coding-plan-limits.js +104 -27
- package/src/lib/codex-model-attribution.js +128 -0
- package/src/lib/codex-rollout-parser.js +98 -4
- package/src/lib/pricing/curated-overrides.json +24 -6
- package/src/lib/pricing/index.js +38 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +50 -9
- package/src/lib/session-analytics.js +374 -110
- package/src/lib/usage-limits.js +19 -5
- package/dashboard/dist/assets/DialogDescription-CQuJCP4L.js +0 -1
- package/dashboard/dist/assets/LimitsPage-BJW3teYA.js +0 -5
- package/dashboard/dist/assets/SessionsPage-ytOFN1h9.js +0 -2
- package/dashboard/dist/assets/limits-providers-CjMU709b.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BlOkflqU.js +0 -1
|
@@ -0,0 +1,437 @@
|
|
|
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 } = 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
|
+
signal,
|
|
319
|
+
killProcessGroup: true,
|
|
320
|
+
platform,
|
|
321
|
+
useShell: platform === "win32",
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const usageTimeoutMs = budgetedTimeoutMs(ARK_USAGE_PLAN_TIMEOUT_MS);
|
|
325
|
+
if (usageTimeoutMs <= 0) {
|
|
326
|
+
return { configured: true, error: "Ark Agent Plan provider timed out before arkcli could run." };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = await runCommand(
|
|
330
|
+
commandRunner,
|
|
331
|
+
arkcliPath,
|
|
332
|
+
["usage", "plan", "--format", "json"],
|
|
333
|
+
{ ...commandOptions, timeout: usageTimeoutMs },
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const failWithCache = async (message) => {
|
|
337
|
+
const profileTimeoutMs = budgetedTimeoutMs(ARK_PROFILE_SHOW_TIMEOUT_MS);
|
|
338
|
+
const profileIdentity = profileTimeoutMs > 0
|
|
339
|
+
? await runCommand(
|
|
340
|
+
commandRunner,
|
|
341
|
+
arkcliPath,
|
|
342
|
+
["profile", "show", "--format", "json"],
|
|
343
|
+
{ ...commandOptions, timeout: profileTimeoutMs },
|
|
344
|
+
).then((profileResult) => {
|
|
345
|
+
if (profileResult?.error || profileResult?.status !== 0) return null;
|
|
346
|
+
try {
|
|
347
|
+
return arkProfileIdentity(JSON.parse(String(profileResult.stdout || "")));
|
|
348
|
+
} catch (_error) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}).catch(() => null)
|
|
352
|
+
: null;
|
|
353
|
+
const cached = readArkAgentPlanLimitsCache({ home, nowMs, profileIdentity });
|
|
354
|
+
if (cached) return cached;
|
|
355
|
+
return { configured: true, error: message };
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (result?.error || result?.status !== 0) {
|
|
359
|
+
const isExit127 = result?.status === 127;
|
|
360
|
+
const baseDetail = result?.error?.message
|
|
361
|
+
|| (result?.status !== 0 && result?.status !== null
|
|
362
|
+
? `arkcli exited with code ${result.status}`
|
|
363
|
+
: "arkcli usage plan failed");
|
|
364
|
+
const detail = isExit127
|
|
365
|
+
? "arkcli exited with code 127 — please update arkcli: npm i -g @volcengine/ark-cli"
|
|
366
|
+
: baseDetail;
|
|
367
|
+
const stderr = trimStderr(result?.stderr);
|
|
368
|
+
const message = stderr && !isExit127 ? `${detail}: ${stderr}` : detail;
|
|
369
|
+
return failWithCache(message);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let body;
|
|
373
|
+
try {
|
|
374
|
+
body = JSON.parse(String(result?.stdout || ""));
|
|
375
|
+
} catch (_error) {
|
|
376
|
+
return failWithCache("arkcli usage plan returned invalid JSON.");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let limits;
|
|
380
|
+
try {
|
|
381
|
+
limits = normalizeArkAgentPlanResponse(body);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return failWithCache(error?.message || "Ark Agent Plan response could not be parsed.");
|
|
384
|
+
}
|
|
385
|
+
if (!limits) {
|
|
386
|
+
const entry = Array.isArray(body?.items)
|
|
387
|
+
? body.items.find(
|
|
388
|
+
(candidate) =>
|
|
389
|
+
ARK_PERSONAL_PRODUCTS.includes(candidate?.product) && candidate?.subscribed === false,
|
|
390
|
+
)
|
|
391
|
+
: null;
|
|
392
|
+
if (entry) {
|
|
393
|
+
try {
|
|
394
|
+
fs.unlinkSync(arkAgentPlanCachePath({ home }));
|
|
395
|
+
} catch (_error) {}
|
|
396
|
+
}
|
|
397
|
+
return { configured: false };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!limits.plan_label) {
|
|
401
|
+
const plansTimeoutMs = budgetedTimeoutMs(ARK_USAGE_PLAN_TIMEOUT_MS);
|
|
402
|
+
if (plansTimeoutMs > 0) {
|
|
403
|
+
const plansResult = await runCommand(
|
|
404
|
+
commandRunner,
|
|
405
|
+
arkcliPath,
|
|
406
|
+
["plans", "get", "--format", "json"],
|
|
407
|
+
{ ...commandOptions, timeout: plansTimeoutMs },
|
|
408
|
+
);
|
|
409
|
+
if (!plansResult?.error && plansResult?.status === 0) {
|
|
410
|
+
try {
|
|
411
|
+
const tier = normalizeArkAgentPlansResponse(JSON.parse(String(plansResult.stdout || "")));
|
|
412
|
+
if (tier) limits.plan_label = planLabelForTier(tier);
|
|
413
|
+
} catch (_error) {}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
writeArkAgentPlanLimitsCache(limits, { home, nowMs });
|
|
419
|
+
return limits;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = {
|
|
423
|
+
ARK_PERIOD_WINDOW,
|
|
424
|
+
ARK_PERSONAL_PRODUCTS,
|
|
425
|
+
ARK_PRODUCT_PRIORITY,
|
|
426
|
+
normalizeArkAgentPlansResponse,
|
|
427
|
+
normalizeArkPlansResponse,
|
|
428
|
+
arkProfileIdentity,
|
|
429
|
+
normalizeArkAgentPlanResponse,
|
|
430
|
+
normalizeArkCodingPlanResponse,
|
|
431
|
+
readArkAgentPlanLimitsCache,
|
|
432
|
+
writeArkAgentPlanLimitsCache,
|
|
433
|
+
hasArkCliInstallEvidence,
|
|
434
|
+
fetchArkAgentPlanLimits,
|
|
435
|
+
resolveArkProductItem,
|
|
436
|
+
percentFromPeriod,
|
|
437
|
+
};
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// Ark Coding Plan (火山方舟 Coding Plan) quota monitoring.
|
|
4
4
|
//
|
|
5
|
-
// Coding Plan
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
5
|
+
// Coding Plan (Lite/Pro) and Agent Plan (Small/Medium/Large/Max) are
|
|
6
|
+
// two parallel subscription products that each refresh on three windows —
|
|
7
|
+
// 5-hour (session for Coding Plan, 5h for Agent 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. Agent Plan is handled by a separate provider module
|
|
15
|
+
// `ark-agent-plan-limits.js` (dual Provider, issue #555).
|
|
12
16
|
//
|
|
13
17
|
// The quota is read through the user's own `arkcli` binary
|
|
14
18
|
// (`arkcli usage plan --format json`), which is already installed and logged
|
|
@@ -43,14 +47,23 @@ const ARK_PROVIDER_TIMEOUT_MS = 15_000;
|
|
|
43
47
|
// Same shape as codexResetCreditListTimeoutMs's guard in usage-limits.js.
|
|
44
48
|
const ARK_PROVIDER_BUDGET_GUARD_MS = 1_500;
|
|
45
49
|
|
|
46
|
-
// arkcli period label -> canonical window slot.
|
|
47
|
-
//
|
|
50
|
+
// arkcli period label -> canonical window slot.
|
|
51
|
+
// Coding Plan uses `session` for the 5-hour rolling window; Agent Plan
|
|
52
|
+
// uses `5h`. `weekly` and `monthly` are shared. `daily` (Agent Plan
|
|
53
|
+
// visual quota) is intentionally not surfaced — it is a separate
|
|
54
|
+
// per-day visual bucket that does not fit the three-window panel.
|
|
48
55
|
const ARK_PERIOD_WINDOW = {
|
|
49
56
|
session: "primary_window",
|
|
57
|
+
"5h": "primary_window",
|
|
50
58
|
weekly: "secondary_window",
|
|
51
59
|
monthly: "tertiary_window",
|
|
52
60
|
};
|
|
53
61
|
|
|
62
|
+
// Coding Plan personal product. Team products (coding-plan-team) are
|
|
63
|
+
// seat-scoped and not surfaced in this personal-quota panel.
|
|
64
|
+
const ARK_PERSONAL_PRODUCTS = ["coding-plan"];
|
|
65
|
+
const ARK_PRODUCT_PRIORITY = ["coding-plan"];
|
|
66
|
+
|
|
54
67
|
function clampPercent(value) {
|
|
55
68
|
return Math.max(0, Math.min(100, value));
|
|
56
69
|
}
|
|
@@ -73,22 +86,33 @@ function normalizeResetAt(value) {
|
|
|
73
86
|
// "Ark Coding Plan Coding Plan Lite".
|
|
74
87
|
function planLabelForTier(tier) {
|
|
75
88
|
const normalized = String(tier || "").trim().toLowerCase();
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
const mapping = {
|
|
90
|
+
pro: "Pro",
|
|
91
|
+
lite: "Lite",
|
|
92
|
+
small: "Small",
|
|
93
|
+
medium: "Medium",
|
|
94
|
+
large: "Large",
|
|
95
|
+
max: "Max",
|
|
96
|
+
};
|
|
97
|
+
if (mapping[normalized]) return mapping[normalized];
|
|
78
98
|
return tier && String(tier).trim() ? String(tier).trim() : null;
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/**
|
|
82
102
|
* Normalize the JSON payload returned by `arkcli plans get --format json`.
|
|
83
|
-
* The tier ("lite" / "pro") lives on the plans payload,
|
|
84
|
-
* payload, so it is resolved here and merged into the
|
|
103
|
+
* The tier ("lite" / "pro" for Coding Plan) lives on the plans payload,
|
|
104
|
+
* not on the usage payload, so it is resolved here and merged into the
|
|
105
|
+
* plan label.
|
|
85
106
|
* Returns null when there is no Coding Plan entry.
|
|
86
107
|
*/
|
|
87
108
|
function normalizeArkPlansResponse(body) {
|
|
88
109
|
if (!body || typeof body !== "object") return null;
|
|
89
110
|
const plans = Array.isArray(body.plans) ? body.plans : [];
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
for (const key of ARK_PRODUCT_PRIORITY) {
|
|
112
|
+
const plan = plans.find((entry) => entry?.key === key);
|
|
113
|
+
if (plan?.tier) return String(plan.tier);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
function arkProfileIdentity(body) {
|
|
@@ -114,26 +138,56 @@ function arkProfileIdentity(body) {
|
|
|
114
138
|
return identity || null;
|
|
115
139
|
}
|
|
116
140
|
|
|
141
|
+
function resolveArkProductItem(items) {
|
|
142
|
+
for (const product of ARK_PRODUCT_PRIORITY) {
|
|
143
|
+
const item = items.find((entry) => entry?.product === product && entry?.subscribed === true);
|
|
144
|
+
if (item) return item;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function percentFromPeriod(period) {
|
|
150
|
+
// null / undefined / blank percent must fall through to the used/total
|
|
151
|
+
// fallback: Number(null) and Number("") both coerce to 0, which would
|
|
152
|
+
// otherwise report a fake fresh 0% window instead of deriving real usage
|
|
153
|
+
// (Agent Plan's 5h period can omit percent entirely, #555).
|
|
154
|
+
const rawPercent = period?.percent;
|
|
155
|
+
if (rawPercent !== null && rawPercent !== undefined && String(rawPercent).trim() !== "") {
|
|
156
|
+
const direct = Number(rawPercent);
|
|
157
|
+
if (Number.isFinite(direct)) return direct;
|
|
158
|
+
}
|
|
159
|
+
const used = Number(period?.used);
|
|
160
|
+
const total = Number(period?.total);
|
|
161
|
+
if (Number.isFinite(used) && Number.isFinite(total) && total > 0) {
|
|
162
|
+
return (used / total) * 100;
|
|
163
|
+
}
|
|
164
|
+
return NaN;
|
|
165
|
+
}
|
|
166
|
+
|
|
117
167
|
/**
|
|
118
168
|
* Normalize the JSON payload returned by `arkcli usage plan --format json`.
|
|
119
169
|
* Returns `null` when the account has no active Coding Plan subscription
|
|
120
170
|
* (caller reports `configured: false`). Throws when the payload shape is
|
|
121
171
|
* unusable so the caller can fall back to the disk cache.
|
|
172
|
+
* Only `coding-plan` (Lite/Pro, session label) is handled here;
|
|
173
|
+
* Agent Plan is handled by `ark-agent-plan-limits.js`.
|
|
122
174
|
*/
|
|
123
175
|
function normalizeArkCodingPlanResponse(body) {
|
|
124
176
|
if (!body || typeof body !== "object") {
|
|
125
177
|
throw new Error("Ark Coding Plan response is not an object.");
|
|
126
178
|
}
|
|
127
179
|
const items = Array.isArray(body.items) ? body.items : [];
|
|
128
|
-
const item = items
|
|
129
|
-
if (!item
|
|
180
|
+
const item = resolveArkProductItem(items);
|
|
181
|
+
if (!item) return null;
|
|
130
182
|
|
|
131
183
|
const windows = {};
|
|
132
184
|
const periods = Array.isArray(item.periods) ? item.periods : [];
|
|
133
185
|
for (const period of periods) {
|
|
186
|
+
// Agent Plan `daily` is a visual-only quota not shown in the 3-window panel.
|
|
187
|
+
if (period?.label === "daily") continue;
|
|
134
188
|
const slot = ARK_PERIOD_WINDOW[period?.label];
|
|
135
189
|
if (!slot) continue;
|
|
136
|
-
const percent =
|
|
190
|
+
const percent = percentFromPeriod(period);
|
|
137
191
|
if (!Number.isFinite(percent)) continue;
|
|
138
192
|
windows[slot] = {
|
|
139
193
|
used_percent: clampPercent(percent),
|
|
@@ -149,6 +203,7 @@ function normalizeArkCodingPlanResponse(body) {
|
|
|
149
203
|
configured: true,
|
|
150
204
|
error: null,
|
|
151
205
|
plan_label: planLabelForTier(item.tier),
|
|
206
|
+
product: item.product || null,
|
|
152
207
|
primary_window: windows.primary_window || null,
|
|
153
208
|
secondary_window: windows.secondary_window || null,
|
|
154
209
|
tertiary_window: windows.tertiary_window || null,
|
|
@@ -194,6 +249,7 @@ function readArkCodingPlanLimitsCache({ home = os.homedir(), nowMs = Date.now(),
|
|
|
194
249
|
configured: true,
|
|
195
250
|
error: null,
|
|
196
251
|
plan_label: typeof parsed?.plan_label === "string" ? parsed.plan_label : null,
|
|
252
|
+
product: typeof parsed?.product === "string" ? parsed.product : null,
|
|
197
253
|
primary_window: bounded[0],
|
|
198
254
|
secondary_window: bounded[1],
|
|
199
255
|
tertiary_window: bounded[2],
|
|
@@ -212,6 +268,7 @@ function writeArkCodingPlanLimitsCache(limits, { home = os.homedir(), nowMs = Da
|
|
|
212
268
|
const cachePath = arkCodingPlanCachePath({ home });
|
|
213
269
|
const payload = {
|
|
214
270
|
plan_label: limits.plan_label || null,
|
|
271
|
+
product: limits.product || null,
|
|
215
272
|
profile_identity: limits.profile_identity || null,
|
|
216
273
|
primary_window: limits.primary_window || null,
|
|
217
274
|
secondary_window: limits.secondary_window || null,
|
|
@@ -384,12 +441,22 @@ async function fetchArkCodingPlanLimits({
|
|
|
384
441
|
};
|
|
385
442
|
|
|
386
443
|
if (result?.error || result?.status !== 0) {
|
|
387
|
-
const
|
|
444
|
+
const isExit127 = result?.status === 127;
|
|
445
|
+
const baseDetail = result?.error?.message
|
|
388
446
|
|| (result?.status !== 0 && result?.status !== null
|
|
389
447
|
? `arkcli exited with code ${result.status}`
|
|
390
448
|
: "arkcli usage plan failed");
|
|
449
|
+
// 127 is the classic "command not found" — on a machine with arkcli
|
|
450
|
+
// installed it almost always means the installed arkcli is too old to
|
|
451
|
+
// support `usage plan --format json` or the binary shim is broken.
|
|
452
|
+
// Surface an actionable hint while still honoring the disk-cache fallback.
|
|
453
|
+
const detail = isExit127
|
|
454
|
+
? "arkcli exited with code 127 — please update arkcli: npm i -g @volcengine/ark-cli"
|
|
455
|
+
: baseDetail;
|
|
391
456
|
const stderr = trimStderr(result?.stderr);
|
|
392
|
-
|
|
457
|
+
// Avoid duplicating the stale 127 hint when stderr already contains it.
|
|
458
|
+
const message = stderr && !isExit127 ? `${detail}: ${stderr}` : detail;
|
|
459
|
+
return failWithCache(message);
|
|
393
460
|
}
|
|
394
461
|
|
|
395
462
|
let body;
|
|
@@ -406,17 +473,22 @@ async function fetchArkCodingPlanLimits({
|
|
|
406
473
|
return failWithCache(error?.message || "Ark Coding Plan response could not be parsed.");
|
|
407
474
|
}
|
|
408
475
|
if (!limits) {
|
|
409
|
-
// Only a response that explicitly carries
|
|
476
|
+
// Only a response that explicitly carries an Ark product entry with
|
|
410
477
|
// `subscribed: false` confirms the plan was retired — drop the cache
|
|
411
478
|
// then, or a later transient CLI failure would resurrect the retired
|
|
412
|
-
// plan's numbers through failWithCache. A payload with *no*
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
479
|
+
// plan's numbers through failWithCache. A payload with *no* Ark entry
|
|
480
|
+
// at all (`{}`, `{"items":[]}` — not logged in, backend degraded,
|
|
481
|
+
// product key renamed) is ambiguous and must NOT destroy the cache:
|
|
482
|
+
// transient signals never drive persistent state.
|
|
483
|
+
// Only coding-plan is considered here; agent-plan is handled by the
|
|
484
|
+
// parallel provider `ark-agent-plan-limits.js`.
|
|
416
485
|
const entry = Array.isArray(body?.items)
|
|
417
|
-
? body.items.find(
|
|
486
|
+
? body.items.find(
|
|
487
|
+
(candidate) =>
|
|
488
|
+
ARK_PERSONAL_PRODUCTS.includes(candidate?.product) && candidate?.subscribed === false,
|
|
489
|
+
)
|
|
418
490
|
: null;
|
|
419
|
-
if (entry
|
|
491
|
+
if (entry) {
|
|
420
492
|
try {
|
|
421
493
|
fs.unlinkSync(arkCodingPlanCachePath({ home }));
|
|
422
494
|
} catch (_error) {}
|
|
@@ -452,6 +524,8 @@ async function fetchArkCodingPlanLimits({
|
|
|
452
524
|
|
|
453
525
|
module.exports = {
|
|
454
526
|
ARK_PERIOD_WINDOW,
|
|
527
|
+
ARK_PERSONAL_PRODUCTS,
|
|
528
|
+
ARK_PRODUCT_PRIORITY,
|
|
455
529
|
normalizeArkPlansResponse,
|
|
456
530
|
arkProfileIdentity,
|
|
457
531
|
normalizeArkCodingPlanResponse,
|
|
@@ -459,4 +533,7 @@ module.exports = {
|
|
|
459
533
|
writeArkCodingPlanLimitsCache,
|
|
460
534
|
hasArkCliInstallEvidence,
|
|
461
535
|
fetchArkCodingPlanLimits,
|
|
536
|
+
// exposed for testing
|
|
537
|
+
resolveArkProductItem,
|
|
538
|
+
percentFromPeriod,
|
|
462
539
|
};
|