tokentracker-cli 0.96.1 → 0.96.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +34 -25
- package/README.ja.md +38 -27
- package/README.ko.md +39 -28
- package/README.md +36 -28
- package/README.zh-CN.md +36 -25
- package/dashboard/dist/assets/{AchievementBadge-DKyElm0T.js → AchievementBadge-BX4iBBYM.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BvCx3wND.js → AchievementsPage-DRYRuCbh.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DN69LzXX.js → AchievementsSection-BM_JWWhJ.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DGiKdZfz.js → BadgeDetailModal-DxoxXst2.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-zN8rH7_K.js → BotAnimated-BVpFxG8u.js} +1 -1
- package/dashboard/dist/assets/{Card-COWo-ClV.js → Card-BJJs701t.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BWsB7Eat.js → ClawdAnimated-46wPZ_Tc.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-BfvIwuoJ.js → CommandPalette-B_zkc7IU.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BQWg7R_y.js → CommunityStatsModal-M2k5rIf1.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-C7ABSI-O.js → ConfirmModal-DBvOTcAQ.js} +1 -1
- package/dashboard/dist/assets/{Controls-vMzEZzej.js → Controls-BCkjPMSH.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-3bHhou09.js +1 -0
- package/dashboard/dist/assets/{DevicePage-3ejUBLWM.js → DevicePage-DFBUce8C.js} +1 -1
- package/dashboard/dist/assets/DialogClose-kvDd8h3V.js +1 -0
- package/dashboard/dist/assets/DialogDescription-BMyOicfz.js +1 -0
- package/dashboard/dist/assets/DialogTitle-DL5nvD31.js +1 -0
- package/dashboard/dist/assets/{FadeIn-D7Jcfz1N.js → FadeIn-B-Bg6SEy.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-D--Rx5BS.js → HeaderGithubStar-BExz0hh4.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-n1W0z3Wr.js → HoverTooltip-Dj5zemYT.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-D202tl9B.js → IpCheckPage-DN7FuJ_v.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-D6yL2EZo.js → LandingPage-9dKgSBRy.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-ChFg2mtA.js → LeaderboardAvatar-00V95XIg.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BXHwGs8r.js → LeaderboardPage-DkKfUt0Z.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-B5LoOpCk.js → LeaderboardProfileModal-C9sIHBgN.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-jsukhRz8.js → LeaderboardProfilePage-BAiLMsEb.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-BuenTaHa.js +8 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Du0lz657.js → LocalOnlyNotice-CKhY27Lr.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BFEX9x7-.js → LoginCard-BbvoBx5N.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DqC7_d35.js → LoginModal-P7zIkBhO.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-Dzx8N7HE.js → LoginPage-Bo0bR26x.js} +1 -1
- package/dashboard/dist/assets/PetPage-C6i9bSSl.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-h0gf36vg.js +1 -0
- package/dashboard/dist/assets/ProviderIcon-Dm8fwPdj.js +1 -0
- package/dashboard/dist/assets/{ResetPasswordPage-x_GR79sR.js → ResetPasswordPage-DvSJ8kg0.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BxDiDV4R.js → SegmentedControl-CP7oiGZE.js} +1 -1
- package/dashboard/dist/assets/Select-iwP92x9C.js +1 -0
- package/dashboard/dist/assets/SelectItemText-DzljA15_.js +1 -0
- package/dashboard/dist/assets/{ServiceStatusPage-BVQuVdjg.js → ServiceStatusPage-uD4D0Tid.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-DE39wNIO.js → SessionsPage-Cmb1ssNP.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-D8ryg1Pv.js +2 -0
- package/dashboard/dist/assets/SkillsPage-eXI09q03.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-BPM779Wb.js → TokenGalaxy-f2djrlUv.js} +1 -1
- package/dashboard/dist/assets/ToolbarRootContext-CDxCPYLv.js +1 -0
- package/dashboard/dist/assets/{TrendMonitor-BkNy-o1D.js → TrendMonitor-C419X22G.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Bpg6H-ZB.js → WidgetsPage-AYq__pOO.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C6-NM-RO.js → WrappedPage-Bsm9k5XI.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CvMMIjhi.css → __vite-browser-external-5P7me0Qq.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-BDkeh90N.js +45 -0
- package/dashboard/dist/assets/{agent-logos-DpbkRqXT.js → agent-logos-B6Vg-4_1.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BjOtWUVs.js → arrow-up-right-DIQXwaOD.js} +1 -1
- package/dashboard/dist/assets/{check-BlxnX5jZ.js → check-CgD_bV39.js} +1 -1
- package/dashboard/dist/assets/composite-BIgEqWZD.js +1 -0
- package/dashboard/dist/assets/{copy-CIsWjLY5.js → copy-DlM1BHdp.js} +1 -1
- package/dashboard/dist/assets/{download-QDaQbQdd.js → download-CQFG-VIY.js} +1 -1
- package/dashboard/dist/assets/{external-link-DPV4VU3m.js → external-link-BQyDqGxz.js} +1 -1
- package/dashboard/dist/assets/{flame-B_X-7VNp.js → flame-BamzyXzF.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DEx2TJj8.js → format-tokens-C4G-_xYt.js} +1 -1
- package/dashboard/dist/assets/{icons-983RyGSl.js → icons-Ntscvl1a.js} +1 -1
- package/dashboard/dist/assets/{index-qtonw_x4.js → index-CUrcPqgp.js} +1 -1
- package/dashboard/dist/assets/{index-BONYA8HZ.js → index-DGxgsNoQ.js} +1 -1
- package/dashboard/dist/assets/{index-Cjs5stC0.js → index-FRIt5mlv.js} +1 -1
- package/dashboard/dist/assets/{info-DpVOhXZI.js → info-CXytWcZi.js} +1 -1
- package/dashboard/dist/assets/{link-2-CBro-upz.js → link-2-Dexoo_jO.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-wYFdSyOd.js → loader-circle-Cn0JHc8t.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-DhTyQ4ly.js → maximize-2-DLh7gYuP.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CPljwVhN.js → modifiers.esm-DOh_xtyt.js} +1 -1
- package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-D_wr-HOR.js → monitor-smartphone-CpqS9Ior.js} +1 -1
- package/dashboard/dist/assets/{plus-BNjfkBEF.js → plus-bfQV7nyv.js} +1 -1
- package/dashboard/dist/assets/provider-display-Bf9pW06U.js +42 -0
- package/dashboard/dist/assets/{react-yHv9MTUA.js → react-BmFZKA_b.js} +1 -1
- package/dashboard/dist/assets/{search-Dfak92ZA.js → search-l3jJ2fzC.js} +1 -1
- package/dashboard/dist/assets/{skills-api-By2PdNa1.js → skills-api-CWmLnaxn.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-B9xy66_v.js → sortable.esm-DLgyXLHM.js} +1 -1
- package/dashboard/dist/assets/store-JM-a8ElZ.js +1 -0
- package/dashboard/dist/assets/{terminal-pbXWBmZM.js → terminal-6aY2Sn8P.js} +1 -1
- package/dashboard/dist/assets/{trash-2-CboDPwbW.js → trash-D5oon5pT.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-D6HTdnHq.js → use-community-stats-ZWUMFwzc.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-hwSr9Cam.js → use-latest-request-guard-BDaXr4gK.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-yXXToNIc.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-OtfOqrNb.js → use-ordered-list-C6RG8N6c.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-BqDKim_3.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BScvtS8F.js → use-session-efficiency-pref-DpgZO_Lx.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Cy1ykjEs.js → use-transform-D42P7jkd.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CV6puFwq.js +1 -0
- package/dashboard/dist/assets/{useCurrency-VqF3cy1C.js → useCurrency-GWBmOrCR.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-Cgub4MXu.js → useTokenFormat-Beybkb0v.js} +1 -1
- package/dashboard/dist/assets/{zap-DStzvQJI.js → zap-CEq2616W.js} +1 -1
- package/dashboard/dist/brand-logos/acode-dark.png +0 -0
- package/dashboard/dist/brand-logos/acode.png +0 -0
- package/dashboard/dist/brand-logos/commandcode.svg +1 -0
- package/dashboard/dist/brand-logos/devin.svg +6 -0
- package/dashboard/dist/index.html +20 -16
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +4 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +114 -4
- package/src/commands/status.js +37 -0
- package/src/commands/sync.js +92 -10
- package/src/commands/uninstall.js +23 -0
- package/src/lib/codex-config.js +42 -0
- package/src/lib/codex-rollout-parser.js +2 -2
- package/src/lib/commandcode-limits.js +317 -0
- package/src/lib/devin-limits.js +249 -0
- package/src/lib/devin-usage.js +212 -0
- package/src/lib/local-api.js +14 -0
- package/src/lib/passive-mode.js +13 -1
- package/src/lib/pricing/curated-overrides.json +78 -1
- package/src/lib/pricing/index.js +11 -9
- package/src/lib/pricing/matcher.js +28 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +1473 -147
- package/src/lib/session-analytics.js +2 -2
- package/src/lib/skills-manager.js +5 -0
- package/src/lib/subscriptions.js +70 -16
- package/src/lib/usage-limits.js +240 -49
- package/dashboard/dist/assets/DashboardPage-QBhbCMZF.js +0 -1
- package/dashboard/dist/assets/DialogClose-D_dvtfGV.js +0 -1
- package/dashboard/dist/assets/DialogDescription-ryGYdQjX.js +0 -1
- package/dashboard/dist/assets/DialogTitle-_ahf0z8b.js +0 -1
- package/dashboard/dist/assets/LimitsPage-PvqkOOwp.js +0 -6
- package/dashboard/dist/assets/PetPage-CNlWHF4z.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-D9jadD3j.js +0 -1
- package/dashboard/dist/assets/ProviderIcon-Caz5PuAm.js +0 -1
- package/dashboard/dist/assets/Select-B5o-1sEl.js +0 -1
- package/dashboard/dist/assets/SelectItemText-CiA6L7Wk.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DJrb_fsK.js +0 -2
- package/dashboard/dist/assets/SkillsPage-COk-YQQ5.js +0 -1
- package/dashboard/dist/assets/ToolbarRootContext-CozJT6Vs.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-BGnq8Ebm.js +0 -45
- package/dashboard/dist/assets/composite-Dxmivlk4.js +0 -1
- package/dashboard/dist/assets/limits-providers-BUoquSA-.js +0 -1
- package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
- package/dashboard/dist/assets/provider-display-BOBtUyFi.js +0 -42
- package/dashboard/dist/assets/store-oQZR4d9M.js +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-B-pbwUS8.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DGXcZBX3.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-CSMb2zYW.js +0 -1
package/src/lib/codex-config.js
CHANGED
|
@@ -19,6 +19,10 @@ function buildEveryCodeNotifyCmd(notifyPath, options) {
|
|
|
19
19
|
return [...buildCodexNotifyCmd(notifyPath, options), "--source=every-code"];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function buildAcodeNotifyCmd(notifyPath, options) {
|
|
23
|
+
return [...buildCodexNotifyCmd(notifyPath, options), "--source=acode"];
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
// Is this notify command one we wrote? Strict array equality is not a safe
|
|
23
27
|
// test, because the command is not a stable constant across invocations:
|
|
24
28
|
// - argv[0] on Windows is an absolute `process.execPath` (#361). The desktop
|
|
@@ -172,6 +176,39 @@ async function readCodexNotify(codexConfigPath) {
|
|
|
172
176
|
return readNotify(codexConfigPath);
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
async function upsertAcodeNotify({
|
|
180
|
+
acodeConfigPath,
|
|
181
|
+
notifyCmd,
|
|
182
|
+
notifyOriginalPath,
|
|
183
|
+
captureOriginal = true,
|
|
184
|
+
replaceOriginal = false,
|
|
185
|
+
}) {
|
|
186
|
+
return upsertNotify({
|
|
187
|
+
configPath: acodeConfigPath,
|
|
188
|
+
notifyCmd,
|
|
189
|
+
notifyOriginalPath,
|
|
190
|
+
configLabel: "AStudio config",
|
|
191
|
+
captureOriginal,
|
|
192
|
+
replaceOriginal,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function restoreAcodeNotify({ acodeConfigPath, notifyOriginalPath, notifyCmd }) {
|
|
197
|
+
return restoreNotify({
|
|
198
|
+
configPath: acodeConfigPath,
|
|
199
|
+
notifyOriginalPath,
|
|
200
|
+
expectedNotify: notifyCmd,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function loadAcodeNotifyOriginal(notifyOriginalPath) {
|
|
205
|
+
return loadNotifyOriginal(notifyOriginalPath);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function readAcodeNotify(acodeConfigPath) {
|
|
209
|
+
return readNotify(acodeConfigPath);
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
async function upsertEveryCodeNotify({
|
|
176
213
|
codeConfigPath,
|
|
177
214
|
notifyCmd,
|
|
@@ -536,6 +573,7 @@ function arraysEqual(a, b) {
|
|
|
536
573
|
|
|
537
574
|
module.exports = {
|
|
538
575
|
buildCodexNotifyCmd,
|
|
576
|
+
buildAcodeNotifyCmd,
|
|
539
577
|
buildEveryCodeNotifyCmd,
|
|
540
578
|
isManagedNotifyCmd,
|
|
541
579
|
upsertNotify,
|
|
@@ -546,6 +584,10 @@ module.exports = {
|
|
|
546
584
|
restoreCodexNotify,
|
|
547
585
|
loadCodexNotifyOriginal,
|
|
548
586
|
readCodexNotify,
|
|
587
|
+
upsertAcodeNotify,
|
|
588
|
+
restoreAcodeNotify,
|
|
589
|
+
loadAcodeNotifyOriginal,
|
|
590
|
+
readAcodeNotify,
|
|
549
591
|
upsertEveryCodeNotify,
|
|
550
592
|
restoreEveryCodeNotify,
|
|
551
593
|
loadEveryCodeNotifyOriginal,
|
|
@@ -36,8 +36,8 @@ const MAX_DISCOVERY_INVENTORIES = 32;
|
|
|
36
36
|
const DISCOVERY_INVENTORIES = new Map();
|
|
37
37
|
const ZONED_DAY_FORMATTERS = new Map();
|
|
38
38
|
const CODEX_RESUME_INVALIDATED = "TOKENTRACKER_CODEX_RESUME_INVALIDATED";
|
|
39
|
-
//
|
|
40
|
-
//
|
|
39
|
+
// Match src/lib/pricing/index.js. Classify individual requests using raw
|
|
40
|
+
// input (including cache), not the accumulated session or day totals.
|
|
41
41
|
const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272_000;
|
|
42
42
|
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// CommandCode subscription usage limits.
|
|
2
|
+
//
|
|
3
|
+
// Data source (authoritative, mirrors the CLI's own `/usage` overlay in
|
|
4
|
+
// command-code 1.49.1):
|
|
5
|
+
//
|
|
6
|
+
// GET https://api.commandcode.ai/alpha/whoami?limits=1
|
|
7
|
+
// GET https://api.commandcode.ai/alpha/billing/credits[?orgId=...]
|
|
8
|
+
// GET https://api.commandcode.ai/alpha/billing/subscriptions[?orgId=...]
|
|
9
|
+
//
|
|
10
|
+
// with `Authorization: Bearer <apiKey>`. The credits payload carries
|
|
11
|
+
// `windowLimits: { limited, fiveHour: { used, cap, resetAt }, weekly: {...} }`
|
|
12
|
+
// (resetAt is epoch milliseconds) plus a `credits` balance object; the
|
|
13
|
+
// subscription payload carries `{ data: { planId, status, ... } }`.
|
|
14
|
+
//
|
|
15
|
+
// Auth resolution mirrors the CLI: `COMMAND_CODE_API_KEY` wins, otherwise the
|
|
16
|
+
// `apiKey` field of `~/.commandcode/auth.json` (prod; written 0600 by
|
|
17
|
+
// `cmd login`, never edited by hand). No key -> `{ configured: false }` so the
|
|
18
|
+
// Limits panel hides the section instead of erroring.
|
|
19
|
+
//
|
|
20
|
+
// The API exposes 5h + weekly rolling caps; extra pay-as-you-go credits
|
|
21
|
+
// bypass the caps server-side, so the reported used/cap needs no adjustment.
|
|
22
|
+
|
|
23
|
+
const fs = require("node:fs");
|
|
24
|
+
const path = require("node:path");
|
|
25
|
+
|
|
26
|
+
const COMMANDCODE_API_BASE_URL = "https://api.commandcode.ai";
|
|
27
|
+
const COMMANDCODE_API_KEY_ENV = "COMMAND_CODE_API_KEY";
|
|
28
|
+
const COMMANDCODE_AUTH_DIRNAME = ".commandcode";
|
|
29
|
+
const COMMANDCODE_AUTH_FILENAME = "auth.json";
|
|
30
|
+
|
|
31
|
+
// Request timeouts are enforced by the caller: usage-limits.js passes a
|
|
32
|
+
// withFetchTimeout-wrapped fetchImpl (per-request AbortSignal timeout), so this
|
|
33
|
+
// module takes no timeout of its own.
|
|
34
|
+
|
|
35
|
+
// Rolling window lengths are plan-independent and documented
|
|
36
|
+
// (https://commandcode.ai/docs/resources/usage-limits): 5h + 7d.
|
|
37
|
+
const COMMANDCODE_SESSION_WINDOW_SECONDS = 5 * 60 * 60;
|
|
38
|
+
const COMMANDCODE_WEEKLY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
|
|
39
|
+
|
|
40
|
+
function isNonEmptyString(value) {
|
|
41
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Explicit `home`/env HOME only — never os.homedir() — so a synthetic test env
|
|
45
|
+
// (no home, no HOME) discovers nothing and tests stay isolated from the
|
|
46
|
+
// developer's real ~/.commandcode (cf. resolveOpencodeDataDir).
|
|
47
|
+
function resolveCommandcodeHome({ home, env = process.env } = {}) {
|
|
48
|
+
if (isNonEmptyString(home)) return home.trim();
|
|
49
|
+
const envHome = env && typeof env === "object" ? env.HOME : null;
|
|
50
|
+
if (isNonEmptyString(envHome)) return envHome.trim();
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveCommandcodeAuthPath({ home, env = process.env } = {}) {
|
|
55
|
+
const dir = resolveCommandcodeHome({ home, env });
|
|
56
|
+
if (!dir) return null;
|
|
57
|
+
return path.join(dir, COMMANDCODE_AUTH_DIRNAME, COMMANDCODE_AUTH_FILENAME);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readCommandcodeApiKey({ home, env = process.env } = {}) {
|
|
61
|
+
const fromEnv =
|
|
62
|
+
env && typeof env === "object" ? env[COMMANDCODE_API_KEY_ENV] : null;
|
|
63
|
+
if (isNonEmptyString(fromEnv)) return fromEnv.trim();
|
|
64
|
+
const authPath = resolveCommandcodeAuthPath({ home, env });
|
|
65
|
+
if (!authPath) return null;
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(authPath, "utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(raw);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return isNonEmptyString(parsed?.apiKey) ? parsed.apiKey.trim() : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeResetAt(value) {
|
|
82
|
+
if (value === null || value === undefined || value === "") return null;
|
|
83
|
+
if (
|
|
84
|
+
typeof value === "number" ||
|
|
85
|
+
/^\d+(?:\.\d+)?$/.test(String(value).trim())
|
|
86
|
+
) {
|
|
87
|
+
const raw = Number(value);
|
|
88
|
+
if (!Number.isFinite(raw) || raw <= 0) return null;
|
|
89
|
+
const milliseconds = raw > 10_000_000_000 ? raw : raw * 1000;
|
|
90
|
+
const date = new Date(milliseconds);
|
|
91
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
92
|
+
}
|
|
93
|
+
const timestamp = Date.parse(String(value));
|
|
94
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function clampPercent(value) {
|
|
98
|
+
if (value === null || value === undefined || value === "") return null;
|
|
99
|
+
const n = Number(value);
|
|
100
|
+
if (!Number.isFinite(n)) return null;
|
|
101
|
+
if (n <= 0) return 0;
|
|
102
|
+
if (n >= 100) return 100;
|
|
103
|
+
return n;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildWindow({ usedPercent, resetAt, windowSeconds = null }) {
|
|
107
|
+
const pct = clampPercent(usedPercent);
|
|
108
|
+
if (pct === null) return null;
|
|
109
|
+
const window = {
|
|
110
|
+
used_percent: pct,
|
|
111
|
+
reset_at: typeof resetAt === "string" && resetAt ? resetAt : null,
|
|
112
|
+
};
|
|
113
|
+
if (Number.isFinite(windowSeconds) && windowSeconds > 0) {
|
|
114
|
+
window.limit_window_seconds = windowSeconds;
|
|
115
|
+
}
|
|
116
|
+
return window;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Upstream window rows are `{ used, cap, resetAt }` (epoch ms). Tolerate the
|
|
120
|
+
// snake_case spellings other providers use so a server rename stays a miss on
|
|
121
|
+
// one window, not a crash.
|
|
122
|
+
function normalizeCommandcodeWindow(raw, windowSeconds) {
|
|
123
|
+
if (!raw || typeof raw !== "object") return null;
|
|
124
|
+
// Missing or malformed usage is unknown, not an unused (0%) window.
|
|
125
|
+
const numeric = (value) => typeof value === "number"
|
|
126
|
+
|| (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)));
|
|
127
|
+
if (!numeric(raw.used) || !numeric(raw.cap ?? raw.total ?? raw.limit)) return null;
|
|
128
|
+
const used = Number(raw.used);
|
|
129
|
+
const cap = Number(raw.cap ?? raw.total ?? raw.limit);
|
|
130
|
+
if (!Number.isFinite(used) || !Number.isFinite(cap) || cap <= 0 || used < 0)
|
|
131
|
+
return null;
|
|
132
|
+
return buildWindow({
|
|
133
|
+
usedPercent: (used / cap) * 100,
|
|
134
|
+
resetAt: normalizeResetAt(raw.resetAt ?? raw.reset_at ?? raw.reset),
|
|
135
|
+
windowSeconds,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Plan tiers (longest prefix first, mirroring the CLI's own getPlanInfo
|
|
140
|
+
// matching in command-code). Unknown ids -> null, so the Limits panel renders
|
|
141
|
+
// the bare "Command Code" brand (cf. Kimi/Grok — a raw id like
|
|
142
|
+
// `individual-pro-v1` reads as garbage).
|
|
143
|
+
const COMMANDCODE_PLAN_TIERS = [
|
|
144
|
+
["individual-pro-v1", "Pro"],
|
|
145
|
+
["individual-provider", "Provider"],
|
|
146
|
+
["individual-goat", "GOAT"],
|
|
147
|
+
["individual-go", "Go"],
|
|
148
|
+
["individual-pro", "Pro"],
|
|
149
|
+
["individual-max", "Max"],
|
|
150
|
+
["individual-ultra", "Ultra"],
|
|
151
|
+
["teams-pro", "Teams Pro"],
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
function deriveCommandcodePlanLabel(rawPlanId) {
|
|
155
|
+
if (!isNonEmptyString(rawPlanId)) return null;
|
|
156
|
+
const normalized = rawPlanId.trim().toLowerCase().replace(/_/g, "-");
|
|
157
|
+
for (const [prefix, label] of COMMANDCODE_PLAN_TIERS) {
|
|
158
|
+
if (normalized.startsWith(prefix)) return label;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeCommandcodeWindowLimits(windowLimits) {
|
|
164
|
+
if (!windowLimits || typeof windowLimits !== "object") return null;
|
|
165
|
+
const fiveHour = normalizeCommandcodeWindow(
|
|
166
|
+
windowLimits.fiveHour ?? windowLimits.five_hour,
|
|
167
|
+
COMMANDCODE_SESSION_WINDOW_SECONDS,
|
|
168
|
+
);
|
|
169
|
+
const weekly = normalizeCommandcodeWindow(
|
|
170
|
+
windowLimits.weekly,
|
|
171
|
+
COMMANDCODE_WEEKLY_WINDOW_SECONDS,
|
|
172
|
+
);
|
|
173
|
+
if (!fiveHour && !weekly) return null;
|
|
174
|
+
|
|
175
|
+
return { fiveHour, weekly };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The Bearer apiKey travels on every request, so the origin must be https.
|
|
179
|
+
// Plain http is accepted for loopback only (local staging/dev servers).
|
|
180
|
+
function resolveCommandcodeOrigin(baseUrl) {
|
|
181
|
+
const origin = String(baseUrl || COMMANDCODE_API_BASE_URL).replace(
|
|
182
|
+
/\/+$/,
|
|
183
|
+
"",
|
|
184
|
+
);
|
|
185
|
+
let parsed;
|
|
186
|
+
try {
|
|
187
|
+
parsed = new URL(origin);
|
|
188
|
+
} catch {
|
|
189
|
+
throw new Error("CommandCode API base URL is invalid.");
|
|
190
|
+
}
|
|
191
|
+
const loopback = /^(localhost|127\.\d+\.\d+\.\d+|\[::1\])$/i.test(
|
|
192
|
+
parsed.hostname,
|
|
193
|
+
);
|
|
194
|
+
if (parsed.protocol !== "https:" && !loopback) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"CommandCode API base URL must use https (http is loopback-only).",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return origin;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function commandcodeHeaders(apiKey) {
|
|
203
|
+
return {
|
|
204
|
+
Accept: "application/json",
|
|
205
|
+
Authorization: `Bearer ${apiKey}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function fetchCommandcodeJson({ url, apiKey, fetchImpl, label }) {
|
|
210
|
+
let response;
|
|
211
|
+
try {
|
|
212
|
+
response = await fetchImpl(url, { headers: commandcodeHeaders(apiKey) });
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`CommandCode ${label} request failed: ${error?.message || "network error"}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (response?.status === 401 || response?.status === 403) {
|
|
219
|
+
const error = new Error(
|
|
220
|
+
"CommandCode token expired — run `cmd login` (macOS/Linux) or `cmdc auth login` (Windows) to refresh.",
|
|
221
|
+
);
|
|
222
|
+
error.code = "AUTH_EXPIRED";
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
if (!response?.ok) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`CommandCode ${label} request failed (HTTP ${response?.status ?? "?"}).`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return await response.json();
|
|
232
|
+
} catch {
|
|
233
|
+
throw new Error(`${label} response was not JSON.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function withOrgParam(route, orgId) {
|
|
238
|
+
if (orgId === null || orgId === undefined || String(orgId).trim() === "")
|
|
239
|
+
return route;
|
|
240
|
+
return `${route}?orgId=${encodeURIComponent(String(orgId).trim())}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Returns `{ configured: false }` without credentials, or the normalized limits
|
|
244
|
+
// object on success. Auth expiry throws with `code: "AUTH_EXPIRED"` and every
|
|
245
|
+
// other transport/schema failure throws a readable Error — the aggregator in
|
|
246
|
+
// usage-limits.js converts both into the panel row.
|
|
247
|
+
async function fetchCommandcodeLimits({
|
|
248
|
+
home,
|
|
249
|
+
env = process.env,
|
|
250
|
+
fetchImpl = fetch,
|
|
251
|
+
baseUrl = COMMANDCODE_API_BASE_URL,
|
|
252
|
+
} = {}) {
|
|
253
|
+
const apiKey = readCommandcodeApiKey({ home, env });
|
|
254
|
+
if (!apiKey) return { configured: false };
|
|
255
|
+
|
|
256
|
+
const origin = resolveCommandcodeOrigin(baseUrl);
|
|
257
|
+
const whoami = await fetchCommandcodeJson({
|
|
258
|
+
url: `${origin}/alpha/whoami?limits=1`,
|
|
259
|
+
apiKey,
|
|
260
|
+
fetchImpl,
|
|
261
|
+
label: "account lookup",
|
|
262
|
+
});
|
|
263
|
+
const orgId = whoami?.data?.org?.id ?? whoami?.org?.id ?? null;
|
|
264
|
+
|
|
265
|
+
const [creditsBody, subscriptionBody] = await Promise.all([
|
|
266
|
+
fetchCommandcodeJson({
|
|
267
|
+
url: `${origin}${withOrgParam("/alpha/billing/credits", orgId)}`,
|
|
268
|
+
apiKey,
|
|
269
|
+
fetchImpl,
|
|
270
|
+
label: "credits lookup",
|
|
271
|
+
}),
|
|
272
|
+
fetchCommandcodeJson({
|
|
273
|
+
url: `${origin}${withOrgParam("/alpha/billing/subscriptions", orgId)}`,
|
|
274
|
+
apiKey,
|
|
275
|
+
fetchImpl,
|
|
276
|
+
label: "subscription lookup",
|
|
277
|
+
}),
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
const windows = normalizeCommandcodeWindowLimits(creditsBody?.windowLimits);
|
|
281
|
+
if (!windows) {
|
|
282
|
+
throw new Error("CommandCode credits response is missing windowLimits.");
|
|
283
|
+
}
|
|
284
|
+
const subscription = subscriptionBody?.data ?? subscriptionBody ?? null;
|
|
285
|
+
const planId = isNonEmptyString(subscription?.planId)
|
|
286
|
+
? subscription.planId.trim()
|
|
287
|
+
: null;
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
configured: true,
|
|
291
|
+
error: null,
|
|
292
|
+
plan_label: deriveCommandcodePlanLabel(planId),
|
|
293
|
+
subscription_status: isNonEmptyString(subscription?.status)
|
|
294
|
+
? subscription.status.trim()
|
|
295
|
+
: null,
|
|
296
|
+
primary_window: windows.fiveHour,
|
|
297
|
+
secondary_window: windows.weekly,
|
|
298
|
+
stale: false,
|
|
299
|
+
cached_at: new Date().toISOString(),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
COMMANDCODE_API_BASE_URL,
|
|
305
|
+
COMMANDCODE_API_KEY_ENV,
|
|
306
|
+
COMMANDCODE_SESSION_WINDOW_SECONDS,
|
|
307
|
+
COMMANDCODE_WEEKLY_WINDOW_SECONDS,
|
|
308
|
+
resolveCommandcodeHome,
|
|
309
|
+
resolveCommandcodeAuthPath,
|
|
310
|
+
readCommandcodeApiKey,
|
|
311
|
+
deriveCommandcodePlanLabel,
|
|
312
|
+
normalizeResetAt,
|
|
313
|
+
normalizeCommandcodeWindow,
|
|
314
|
+
normalizeCommandcodeWindowLimits,
|
|
315
|
+
resolveCommandcodeOrigin,
|
|
316
|
+
fetchCommandcodeLimits,
|
|
317
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Devin subscription usage limits.
|
|
2
|
+
//
|
|
3
|
+
// Source: the official seat-management RPC the Devin web app calls,
|
|
4
|
+
// POST https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetPlanStatus
|
|
5
|
+
// authenticated by `x-auth-token` — the session token the Devin CLI stores in
|
|
6
|
+
// $XDG_DATA_HOME/devin/credentials.toml (~/.local/share/devin/ fallback).
|
|
7
|
+
//
|
|
8
|
+
// Two non-obvious contract points, both verified against the official
|
|
9
|
+
// generated descriptor (docs/devin-limits.md):
|
|
10
|
+
// - the quota percent/reset fields are proto3 implicit scalars, so the
|
|
11
|
+
// server omits them at 0: absent percent + live reset = exhausted
|
|
12
|
+
// (100% used), both absent = no such window;
|
|
13
|
+
// - a non-default api_server_url is an unsupported configuration, never a
|
|
14
|
+
// different request destination — the token only ever goes to the fixed
|
|
15
|
+
// host above.
|
|
16
|
+
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
|
|
20
|
+
const DEVIN_PLAN_STATUS_URL =
|
|
21
|
+
"https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetPlanStatus";
|
|
22
|
+
const DEVIN_API_ORIGIN = "https://server.codeium.com";
|
|
23
|
+
|
|
24
|
+
const DEVIN_DAILY_WINDOW_SECONDS = 24 * 60 * 60;
|
|
25
|
+
const DEVIN_WEEKLY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
|
|
26
|
+
|
|
27
|
+
function isNonEmptyString(value) {
|
|
28
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Minimal root-level TOML reader for the CLI-written credentials file: bare
|
|
32
|
+
// `key = value` lines before the first `[section]`, nothing else.
|
|
33
|
+
function parseDevinCredentialsToml(raw) {
|
|
34
|
+
const fields = {};
|
|
35
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
38
|
+
if (trimmed.startsWith("[")) break;
|
|
39
|
+
const match = /^([A-Za-z0-9_]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s#]+))\s*(?:#.*)?$/.exec(
|
|
40
|
+
trimmed,
|
|
41
|
+
);
|
|
42
|
+
if (!match) continue;
|
|
43
|
+
const [, key, doubleQuoted, singleQuoted, bare] = match;
|
|
44
|
+
fields[key] = doubleQuoted ?? singleQuoted ?? bare ?? "";
|
|
45
|
+
}
|
|
46
|
+
return fields;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Returns { apiKey } or null when no sign-in exists (no home, or the file is
|
|
50
|
+
// absent — ENOENT only). Any other read failure or a mangled file throws an
|
|
51
|
+
// owned, actionable error with no path or filesystem details, so a broken
|
|
52
|
+
// sign-in can never masquerade as "not configured".
|
|
53
|
+
function readDevinCredentials({ home, env = process.env } = {}) {
|
|
54
|
+
const xdg = env && typeof env === "object" ? env.XDG_DATA_HOME : null;
|
|
55
|
+
const dir = isNonEmptyString(xdg)
|
|
56
|
+
? xdg.trim()
|
|
57
|
+
: isNonEmptyString(home)
|
|
58
|
+
? home.trim()
|
|
59
|
+
: isNonEmptyString(env?.HOME)
|
|
60
|
+
? env.HOME.trim()
|
|
61
|
+
: null;
|
|
62
|
+
if (!dir) return null;
|
|
63
|
+
const base = isNonEmptyString(xdg)
|
|
64
|
+
? path.join(dir, "devin", "credentials.toml")
|
|
65
|
+
: path.join(dir, ".local", "share", "devin", "credentials.toml");
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(base, "utf8");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error && error.code === "ENOENT") return null;
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Could not read the Devin CLI credentials file — check its permissions or run `devin auth login` to sign in again.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const fields = parseDevinCredentialsToml(raw);
|
|
76
|
+
const apiKey = isNonEmptyString(fields.windsurf_api_key)
|
|
77
|
+
? fields.windsurf_api_key.trim()
|
|
78
|
+
: null;
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"Devin credentials file has no sign-in token — run `devin auth login`.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const apiServerUrl = isNonEmptyString(fields.api_server_url)
|
|
85
|
+
? fields.api_server_url.trim().replace(/\/+$/, "")
|
|
86
|
+
: null;
|
|
87
|
+
if (apiServerUrl && apiServerUrl !== DEVIN_API_ORIGIN) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Devin credentials point at a custom api_server_url, which this quota provider does not support.",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { apiKey };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function malformed(field) {
|
|
96
|
+
return new Error(`Devin quota response has a malformed ${field}.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// undefined → proto-default semantics handled by the caller; null/non-numeric
|
|
100
|
+
// → malformed. Percentages are clamped defensively to 0..100.
|
|
101
|
+
function normalizeRemainingPercent(value) {
|
|
102
|
+
if (value === undefined) return undefined;
|
|
103
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
104
|
+
throw malformed("quota remaining percent");
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
107
|
+
throw malformed("quota remaining percent");
|
|
108
|
+
}
|
|
109
|
+
const n = Number(value);
|
|
110
|
+
if (!Number.isFinite(n)) throw malformed("quota remaining percent");
|
|
111
|
+
if (n <= 0) return 0;
|
|
112
|
+
if (n >= 100) return 100;
|
|
113
|
+
return n;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// int64 unix seconds (JSON string or number). undefined or proto-default 0 →
|
|
117
|
+
// absent; null/non-numeric/non-positive → malformed. Returns epoch ms.
|
|
118
|
+
function normalizeResetMs(value) {
|
|
119
|
+
if (value === undefined || value === 0 || value === "0") return null;
|
|
120
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
121
|
+
throw malformed("quota reset timestamp");
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
124
|
+
throw malformed("quota reset timestamp");
|
|
125
|
+
}
|
|
126
|
+
const n = Number(value);
|
|
127
|
+
if (!Number.isFinite(n) || n <= 0) throw malformed("quota reset timestamp");
|
|
128
|
+
const ms = n * 1000;
|
|
129
|
+
if (!Number.isFinite(new Date(ms).getTime())) {
|
|
130
|
+
throw malformed("quota reset timestamp");
|
|
131
|
+
}
|
|
132
|
+
return ms;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeWindow({ remainingPercent, resetUnix, hidden, windowSeconds }) {
|
|
136
|
+
if (hidden === true) return null;
|
|
137
|
+
const resetMs = normalizeResetMs(resetUnix);
|
|
138
|
+
const remaining = normalizeRemainingPercent(remainingPercent);
|
|
139
|
+
if (remaining === undefined && resetMs === null) return null;
|
|
140
|
+
return {
|
|
141
|
+
used_percent: remaining === undefined ? 100 : 100 - remaining,
|
|
142
|
+
reset_at: resetMs === null ? null : new Date(resetMs).toISOString(),
|
|
143
|
+
limit_window_seconds: windowSeconds,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizePlanStatus(body) {
|
|
148
|
+
const planStatus =
|
|
149
|
+
body && typeof body === "object" ? body.planStatus : undefined;
|
|
150
|
+
if (!planStatus || typeof planStatus !== "object") {
|
|
151
|
+
throw new Error("Devin quota response is missing planStatus.");
|
|
152
|
+
}
|
|
153
|
+
const planInfo =
|
|
154
|
+
planStatus.planInfo && typeof planStatus.planInfo === "object"
|
|
155
|
+
? planStatus.planInfo
|
|
156
|
+
: {};
|
|
157
|
+
const planLabel = isNonEmptyString(planInfo.planName)
|
|
158
|
+
? planInfo.planName.trim()
|
|
159
|
+
: null;
|
|
160
|
+
|
|
161
|
+
// Legacy Windsurf billing fills ACU credit fields instead of the daily /
|
|
162
|
+
// weekly quota; those strategies have nothing renderable here.
|
|
163
|
+
const billingStrategy = planInfo.billingStrategy;
|
|
164
|
+
if (
|
|
165
|
+
billingStrategy !== undefined &&
|
|
166
|
+
billingStrategy !== "BILLING_STRATEGY_QUOTA"
|
|
167
|
+
) {
|
|
168
|
+
return { plan_label: planLabel, primary_window: null, secondary_window: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
plan_label: planLabel,
|
|
173
|
+
primary_window: normalizeWindow({
|
|
174
|
+
remainingPercent: planStatus.dailyQuotaRemainingPercent,
|
|
175
|
+
resetUnix: planStatus.dailyQuotaResetAtUnix,
|
|
176
|
+
hidden: planInfo.hideDailyQuota === true,
|
|
177
|
+
windowSeconds: DEVIN_DAILY_WINDOW_SECONDS,
|
|
178
|
+
}),
|
|
179
|
+
secondary_window: normalizeWindow({
|
|
180
|
+
remainingPercent: planStatus.weeklyQuotaRemainingPercent,
|
|
181
|
+
resetUnix: planStatus.weeklyQuotaResetAtUnix,
|
|
182
|
+
hidden: planInfo.hideWeeklyQuota === true,
|
|
183
|
+
windowSeconds: DEVIN_WEEKLY_WINDOW_SECONDS,
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// `{ configured: false }` when the provider is not enabled or no Devin CLI
|
|
189
|
+
// sign-in exists, otherwise the normalized windows. `enabled` is the user's
|
|
190
|
+
// explicit provider selection forwarded from the local API — without it the
|
|
191
|
+
// credentials file is never opened and no request is made. Throws owned
|
|
192
|
+
// errors only — never raw upstream bodies, filesystem paths or token values.
|
|
193
|
+
// `code: "AUTH_EXPIRED"` flags genuine auth failure for the aggregator's
|
|
194
|
+
// auth_action_required path.
|
|
195
|
+
async function fetchDevinLimits({ home, env = process.env, enabled = false, fetchImpl = fetch } = {}) {
|
|
196
|
+
if (enabled !== true) return { configured: false };
|
|
197
|
+
const credentials = readDevinCredentials({ home, env });
|
|
198
|
+
if (!credentials) return { configured: false };
|
|
199
|
+
|
|
200
|
+
let response;
|
|
201
|
+
try {
|
|
202
|
+
response = await fetchImpl(DEVIN_PLAN_STATUS_URL, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: {
|
|
205
|
+
Accept: "application/json",
|
|
206
|
+
"Content-Type": "application/json",
|
|
207
|
+
"Connect-Protocol-Version": "1",
|
|
208
|
+
"x-auth-token": credentials.apiKey,
|
|
209
|
+
},
|
|
210
|
+
body: "{}",
|
|
211
|
+
// The session token must never reach another origin.
|
|
212
|
+
redirect: "error",
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
error && (error.name === "AbortError" || error.name === "TimeoutError")
|
|
217
|
+
? "Devin quota request timed out."
|
|
218
|
+
: "Devin quota request failed.",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (response?.status === 401 || response?.status === 403) {
|
|
222
|
+
const error = new Error(
|
|
223
|
+
"Devin sign-in expired or was rejected — run `devin auth login` to sign in again.",
|
|
224
|
+
);
|
|
225
|
+
error.code = "AUTH_EXPIRED";
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
if (!response?.ok) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Devin quota request was rejected (HTTP ${response?.status ?? "?"}).`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
let body;
|
|
234
|
+
try {
|
|
235
|
+
body = await response.json();
|
|
236
|
+
} catch {
|
|
237
|
+
throw new Error("Devin quota response was not JSON.");
|
|
238
|
+
}
|
|
239
|
+
const windows = normalizePlanStatus(body);
|
|
240
|
+
return {
|
|
241
|
+
configured: true,
|
|
242
|
+
error: null,
|
|
243
|
+
plan_label: windows.plan_label,
|
|
244
|
+
primary_window: windows.primary_window,
|
|
245
|
+
secondary_window: windows.secondary_window,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = { fetchDevinLimits };
|