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.
Files changed (145) hide show
  1. package/README.de.md +34 -25
  2. package/README.ja.md +38 -27
  3. package/README.ko.md +39 -28
  4. package/README.md +36 -28
  5. package/README.zh-CN.md +36 -25
  6. package/dashboard/dist/assets/{AchievementBadge-DKyElm0T.js → AchievementBadge-BX4iBBYM.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BvCx3wND.js → AchievementsPage-DRYRuCbh.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DN69LzXX.js → AchievementsSection-BM_JWWhJ.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-DGiKdZfz.js → BadgeDetailModal-DxoxXst2.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-zN8rH7_K.js → BotAnimated-BVpFxG8u.js} +1 -1
  11. package/dashboard/dist/assets/{Card-COWo-ClV.js → Card-BJJs701t.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BWsB7Eat.js → ClawdAnimated-46wPZ_Tc.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-BfvIwuoJ.js → CommandPalette-B_zkc7IU.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-BQWg7R_y.js → CommunityStatsModal-M2k5rIf1.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-C7ABSI-O.js → ConfirmModal-DBvOTcAQ.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-vMzEZzej.js → Controls-BCkjPMSH.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-3bHhou09.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-3ejUBLWM.js → DevicePage-DFBUce8C.js} +1 -1
  19. package/dashboard/dist/assets/DialogClose-kvDd8h3V.js +1 -0
  20. package/dashboard/dist/assets/DialogDescription-BMyOicfz.js +1 -0
  21. package/dashboard/dist/assets/DialogTitle-DL5nvD31.js +1 -0
  22. package/dashboard/dist/assets/{FadeIn-D7Jcfz1N.js → FadeIn-B-Bg6SEy.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-D--Rx5BS.js → HeaderGithubStar-BExz0hh4.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-n1W0z3Wr.js → HoverTooltip-Dj5zemYT.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-D202tl9B.js → IpCheckPage-DN7FuJ_v.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-D6yL2EZo.js → LandingPage-9dKgSBRy.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-ChFg2mtA.js → LeaderboardAvatar-00V95XIg.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-BXHwGs8r.js → LeaderboardPage-DkKfUt0Z.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-B5LoOpCk.js → LeaderboardProfileModal-C9sIHBgN.js} +4 -4
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-jsukhRz8.js → LeaderboardProfilePage-BAiLMsEb.js} +1 -1
  31. package/dashboard/dist/assets/LimitsPage-BuenTaHa.js +8 -0
  32. package/dashboard/dist/assets/{LocalOnlyNotice-Du0lz657.js → LocalOnlyNotice-CKhY27Lr.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-BFEX9x7-.js → LoginCard-BbvoBx5N.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-DqC7_d35.js → LoginModal-P7zIkBhO.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-Dzx8N7HE.js → LoginPage-Bo0bR26x.js} +1 -1
  36. package/dashboard/dist/assets/PetPage-C6i9bSSl.js +1 -0
  37. package/dashboard/dist/assets/PopoverPopup-h0gf36vg.js +1 -0
  38. package/dashboard/dist/assets/ProviderIcon-Dm8fwPdj.js +1 -0
  39. package/dashboard/dist/assets/{ResetPasswordPage-x_GR79sR.js → ResetPasswordPage-DvSJ8kg0.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-BxDiDV4R.js → SegmentedControl-CP7oiGZE.js} +1 -1
  41. package/dashboard/dist/assets/Select-iwP92x9C.js +1 -0
  42. package/dashboard/dist/assets/SelectItemText-DzljA15_.js +1 -0
  43. package/dashboard/dist/assets/{ServiceStatusPage-BVQuVdjg.js → ServiceStatusPage-uD4D0Tid.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-DE39wNIO.js → SessionsPage-Cmb1ssNP.js} +2 -2
  45. package/dashboard/dist/assets/SettingsPage-D8ryg1Pv.js +2 -0
  46. package/dashboard/dist/assets/SkillsPage-eXI09q03.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-BPM779Wb.js → TokenGalaxy-f2djrlUv.js} +1 -1
  48. package/dashboard/dist/assets/ToolbarRootContext-CDxCPYLv.js +1 -0
  49. package/dashboard/dist/assets/{TrendMonitor-BkNy-o1D.js → TrendMonitor-C419X22G.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-Bpg6H-ZB.js → WidgetsPage-AYq__pOO.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-C6-NM-RO.js → WrappedPage-Bsm9k5XI.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-CvMMIjhi.css → __vite-browser-external-5P7me0Qq.css} +1 -1
  53. package/dashboard/dist/assets/__vite-browser-external-BDkeh90N.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-DpbkRqXT.js → agent-logos-B6Vg-4_1.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-BjOtWUVs.js → arrow-up-right-DIQXwaOD.js} +1 -1
  56. package/dashboard/dist/assets/{check-BlxnX5jZ.js → check-CgD_bV39.js} +1 -1
  57. package/dashboard/dist/assets/composite-BIgEqWZD.js +1 -0
  58. package/dashboard/dist/assets/{copy-CIsWjLY5.js → copy-DlM1BHdp.js} +1 -1
  59. package/dashboard/dist/assets/{download-QDaQbQdd.js → download-CQFG-VIY.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-DPV4VU3m.js → external-link-BQyDqGxz.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B_X-7VNp.js → flame-BamzyXzF.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-DEx2TJj8.js → format-tokens-C4G-_xYt.js} +1 -1
  63. package/dashboard/dist/assets/{icons-983RyGSl.js → icons-Ntscvl1a.js} +1 -1
  64. package/dashboard/dist/assets/{index-qtonw_x4.js → index-CUrcPqgp.js} +1 -1
  65. package/dashboard/dist/assets/{index-BONYA8HZ.js → index-DGxgsNoQ.js} +1 -1
  66. package/dashboard/dist/assets/{index-Cjs5stC0.js → index-FRIt5mlv.js} +1 -1
  67. package/dashboard/dist/assets/{info-DpVOhXZI.js → info-CXytWcZi.js} +1 -1
  68. package/dashboard/dist/assets/{link-2-CBro-upz.js → link-2-Dexoo_jO.js} +1 -1
  69. package/dashboard/dist/assets/{loader-circle-wYFdSyOd.js → loader-circle-Cn0JHc8t.js} +1 -1
  70. package/dashboard/dist/assets/{maximize-2-DhTyQ4ly.js → maximize-2-DLh7gYuP.js} +1 -1
  71. package/dashboard/dist/assets/{modifiers.esm-CPljwVhN.js → modifiers.esm-DOh_xtyt.js} +1 -1
  72. package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
  73. package/dashboard/dist/assets/{monitor-smartphone-D_wr-HOR.js → monitor-smartphone-CpqS9Ior.js} +1 -1
  74. package/dashboard/dist/assets/{plus-BNjfkBEF.js → plus-bfQV7nyv.js} +1 -1
  75. package/dashboard/dist/assets/provider-display-Bf9pW06U.js +42 -0
  76. package/dashboard/dist/assets/{react-yHv9MTUA.js → react-BmFZKA_b.js} +1 -1
  77. package/dashboard/dist/assets/{search-Dfak92ZA.js → search-l3jJ2fzC.js} +1 -1
  78. package/dashboard/dist/assets/{skills-api-By2PdNa1.js → skills-api-CWmLnaxn.js} +1 -1
  79. package/dashboard/dist/assets/{sortable.esm-B9xy66_v.js → sortable.esm-DLgyXLHM.js} +1 -1
  80. package/dashboard/dist/assets/store-JM-a8ElZ.js +1 -0
  81. package/dashboard/dist/assets/{terminal-pbXWBmZM.js → terminal-6aY2Sn8P.js} +1 -1
  82. package/dashboard/dist/assets/{trash-2-CboDPwbW.js → trash-D5oon5pT.js} +1 -1
  83. package/dashboard/dist/assets/{use-community-stats-D6HTdnHq.js → use-community-stats-ZWUMFwzc.js} +1 -1
  84. package/dashboard/dist/assets/{use-latest-request-guard-hwSr9Cam.js → use-latest-request-guard-BDaXr4gK.js} +1 -1
  85. package/dashboard/dist/assets/use-limits-display-prefs-yXXToNIc.js +1 -0
  86. package/dashboard/dist/assets/{use-ordered-list-OtfOqrNb.js → use-ordered-list-C6RG8N6c.js} +1 -1
  87. package/dashboard/dist/assets/use-reduced-motion-BqDKim_3.js +1 -0
  88. package/dashboard/dist/assets/{use-session-efficiency-pref-BScvtS8F.js → use-session-efficiency-pref-DpgZO_Lx.js} +1 -1
  89. package/dashboard/dist/assets/{use-transform-Cy1ykjEs.js → use-transform-D42P7jkd.js} +1 -1
  90. package/dashboard/dist/assets/use-usage-limits-CV6puFwq.js +1 -0
  91. package/dashboard/dist/assets/{useCurrency-VqF3cy1C.js → useCurrency-GWBmOrCR.js} +1 -1
  92. package/dashboard/dist/assets/{useTokenFormat-Cgub4MXu.js → useTokenFormat-Beybkb0v.js} +1 -1
  93. package/dashboard/dist/assets/{zap-DStzvQJI.js → zap-CEq2616W.js} +1 -1
  94. package/dashboard/dist/brand-logos/acode-dark.png +0 -0
  95. package/dashboard/dist/brand-logos/acode.png +0 -0
  96. package/dashboard/dist/brand-logos/commandcode.svg +1 -0
  97. package/dashboard/dist/brand-logos/devin.svg +6 -0
  98. package/dashboard/dist/index.html +20 -16
  99. package/dashboard/dist/ip-check.html +2 -2
  100. package/dashboard/dist/leaderboard.html +2 -2
  101. package/dashboard/dist/llms.txt +4 -2
  102. package/dashboard/dist/share.html +2 -2
  103. package/package.json +2 -2
  104. package/src/commands/init.js +114 -4
  105. package/src/commands/status.js +37 -0
  106. package/src/commands/sync.js +92 -10
  107. package/src/commands/uninstall.js +23 -0
  108. package/src/lib/codex-config.js +42 -0
  109. package/src/lib/codex-rollout-parser.js +2 -2
  110. package/src/lib/commandcode-limits.js +317 -0
  111. package/src/lib/devin-limits.js +249 -0
  112. package/src/lib/devin-usage.js +212 -0
  113. package/src/lib/local-api.js +14 -0
  114. package/src/lib/passive-mode.js +13 -1
  115. package/src/lib/pricing/curated-overrides.json +78 -1
  116. package/src/lib/pricing/index.js +11 -9
  117. package/src/lib/pricing/matcher.js +28 -0
  118. package/src/lib/pricing/seed-snapshot.json +1 -1
  119. package/src/lib/rollout.js +1473 -147
  120. package/src/lib/session-analytics.js +2 -2
  121. package/src/lib/skills-manager.js +5 -0
  122. package/src/lib/subscriptions.js +70 -16
  123. package/src/lib/usage-limits.js +240 -49
  124. package/dashboard/dist/assets/DashboardPage-QBhbCMZF.js +0 -1
  125. package/dashboard/dist/assets/DialogClose-D_dvtfGV.js +0 -1
  126. package/dashboard/dist/assets/DialogDescription-ryGYdQjX.js +0 -1
  127. package/dashboard/dist/assets/DialogTitle-_ahf0z8b.js +0 -1
  128. package/dashboard/dist/assets/LimitsPage-PvqkOOwp.js +0 -6
  129. package/dashboard/dist/assets/PetPage-CNlWHF4z.js +0 -1
  130. package/dashboard/dist/assets/PopoverPopup-D9jadD3j.js +0 -1
  131. package/dashboard/dist/assets/ProviderIcon-Caz5PuAm.js +0 -1
  132. package/dashboard/dist/assets/Select-B5o-1sEl.js +0 -1
  133. package/dashboard/dist/assets/SelectItemText-CiA6L7Wk.js +0 -1
  134. package/dashboard/dist/assets/SettingsPage-DJrb_fsK.js +0 -2
  135. package/dashboard/dist/assets/SkillsPage-COk-YQQ5.js +0 -1
  136. package/dashboard/dist/assets/ToolbarRootContext-CozJT6Vs.js +0 -1
  137. package/dashboard/dist/assets/__vite-browser-external-BGnq8Ebm.js +0 -45
  138. package/dashboard/dist/assets/composite-Dxmivlk4.js +0 -1
  139. package/dashboard/dist/assets/limits-providers-BUoquSA-.js +0 -1
  140. package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
  141. package/dashboard/dist/assets/provider-display-BOBtUyFi.js +0 -42
  142. package/dashboard/dist/assets/store-oQZR4d9M.js +0 -1
  143. package/dashboard/dist/assets/use-limits-display-prefs-B-pbwUS8.js +0 -1
  144. package/dashboard/dist/assets/use-reduced-motion-DGXcZBX3.js +0 -1
  145. package/dashboard/dist/assets/use-usage-limits-CSMb2zYW.js +0 -1
@@ -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
- // See src/lib/pricing/index.js: no observed Codex request reaches this
40
- // threshold yet (context window 258400), so long_context_* stays at zero.
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 };