tokentracker-cli 0.96.2 → 0.96.4

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 (141) 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-CgEitoX1.js → AchievementBadge-CCcvmz6R.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-dnnea0JH.js → AchievementsPage-C5jdktOK.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-CJqG0UP7.js → AchievementsSection-BFMtUzn1.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-BLV6SJoI.js → BadgeDetailModal-Be8gM7tM.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-Cm78yEbz.js → BotAnimated-CXTGR71z.js} +1 -1
  11. package/dashboard/dist/assets/{Card-IPGSYT6O.js → Card-CsUsp3CV.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-04w1U1sm.js → ClawdAnimated-BEjpZjEE.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-gSSa1V_s.js → CommandPalette-CHc_Z1r8.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-CICwUz14.js → CommunityStatsModal-Gs6kZ_Mp.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-DaWC0cY7.js → ConfirmModal-niHkpv2O.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-BDihA03F.js → Controls-X2Z1UNUx.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-B66s3Exi.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-BPpo_eiY.js → DevicePage-DQdLdEI8.js} +1 -1
  19. package/dashboard/dist/assets/DialogClose-CCUjTxl8.js +1 -0
  20. package/dashboard/dist/assets/DialogDescription-CjMjYG3N.js +1 -0
  21. package/dashboard/dist/assets/DialogTitle-CgkUGAag.js +1 -0
  22. package/dashboard/dist/assets/{FadeIn-DmI9PRLa.js → FadeIn-D4QD1bPo.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-CBO4cY7i.js → HeaderGithubStar-D0my-Z3a.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-CPFGhi9K.js → HoverTooltip-nSWgrKlK.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-Do7w_6z6.js → IpCheckPage-4hfqHAgi.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-at_Ti1ZE.js → LandingPage-wIc4tVRI.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-BH1cnKPp.js → LeaderboardAvatar-DGlN8Vy8.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-Cd30aXwL.js → LeaderboardPage-CybuzE87.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-CVsti_HN.js → LeaderboardProfileModal-D9la3tNd.js} +4 -4
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-BfSpDM9e.js → LeaderboardProfilePage-gxXSeJYb.js} +1 -1
  31. package/dashboard/dist/assets/LimitsPage-CZH3G54Y.js +8 -0
  32. package/dashboard/dist/assets/{LocalOnlyNotice-9-vRgsdn.js → LocalOnlyNotice-Ckc7zohC.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-DM_ZdFNl.js → LoginCard-BqeRHqWK.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-BaNMznwh.js → LoginModal-BjH6aIBG.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-BMMthUf4.js → LoginPage-BOyPLNU3.js} +1 -1
  36. package/dashboard/dist/assets/PetPage-BXRXG40D.js +1 -0
  37. package/dashboard/dist/assets/PopoverPopup-Dad1ZYVd.js +1 -0
  38. package/dashboard/dist/assets/ProviderIcon-BDmrNJSl.js +1 -0
  39. package/dashboard/dist/assets/{ResetPasswordPage-Ca4SULSV.js → ResetPasswordPage-CZpE6qfa.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-Bbsn-qvZ.js → SegmentedControl-CE1hrQFi.js} +1 -1
  41. package/dashboard/dist/assets/Select-YVj7u6Nv.js +1 -0
  42. package/dashboard/dist/assets/SelectItemText-BMYGgQ3o.js +1 -0
  43. package/dashboard/dist/assets/{ServiceStatusPage-BPS8KJ3a.js → ServiceStatusPage-HehjDbZg.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-C1yt3TMZ.js → SessionsPage-Chvu4YqZ.js} +2 -2
  45. package/dashboard/dist/assets/SettingsPage-BXjS7u9_.js +2 -0
  46. package/dashboard/dist/assets/SkillsPage-q3Rjlj9h.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-DKvbJbnv.js → TokenGalaxy-BqZyj82r.js} +1 -1
  48. package/dashboard/dist/assets/ToolbarRootContext-Ch8ueI_C.js +1 -0
  49. package/dashboard/dist/assets/{TrendMonitor-YF5C_-L1.js → TrendMonitor-C2aTL9p1.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-BKV3DgXg.js → WidgetsPage-DYaIai8j.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-s4IRoLf7.js → WrappedPage-51B0v-Sr.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-Cp7dNuCm.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-Hbt-wyl2.js → agent-logos-DXrPSAdX.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-KeIpKlw3.js → arrow-up-right-BFkf7l0U.js} +1 -1
  56. package/dashboard/dist/assets/{check-_QKyY1v_.js → check--N8wmgIJ.js} +1 -1
  57. package/dashboard/dist/assets/composite-zF5Pj94e.js +1 -0
  58. package/dashboard/dist/assets/{copy-DpLM3QMD.js → copy-BG0JMr6A.js} +1 -1
  59. package/dashboard/dist/assets/{download-C4NX-JnW.js → download-CPFJf-Sf.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-CG-7FcOF.js → external-link-D2yF0kgP.js} +1 -1
  61. package/dashboard/dist/assets/{flame-CkWid7Ji.js → flame-l460AFx9.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-CbwWhioZ.js → format-tokens-DKO4ljuH.js} +1 -1
  63. package/dashboard/dist/assets/{icons-BZq17rU3.js → icons-RoxRGkz4.js} +1 -1
  64. package/dashboard/dist/assets/{index-CR9tDiiC.js → index--ouT5L4N.js} +1 -1
  65. package/dashboard/dist/assets/{index-DDGLE0dz.js → index-DhyLCvvv.js} +1 -1
  66. package/dashboard/dist/assets/{index-CSwEiUn4.js → index-PTJ4368-.js} +1 -1
  67. package/dashboard/dist/assets/{info-mkeRjzqc.js → info-DNWMO4Ca.js} +1 -1
  68. package/dashboard/dist/assets/{link-2-BjMw8csC.js → link-2-DzWXAz1h.js} +1 -1
  69. package/dashboard/dist/assets/{loader-circle-cGWOJnWn.js → loader-circle-BzGMqXNu.js} +1 -1
  70. package/dashboard/dist/assets/{maximize-2-Cg_S73tD.js → maximize-2-Dkpyk4x3.js} +1 -1
  71. package/dashboard/dist/assets/{modifiers.esm-DNxTWxId.js → modifiers.esm-lggHUQpW.js} +1 -1
  72. package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
  73. package/dashboard/dist/assets/{monitor-smartphone-BvE9Tt2U.js → monitor-smartphone-Ck0-gz9l.js} +1 -1
  74. package/dashboard/dist/assets/{plus-BC2wdPRr.js → plus-qTJpnn43.js} +1 -1
  75. package/dashboard/dist/assets/provider-display-psY26qmp.js +42 -0
  76. package/dashboard/dist/assets/{react-D0kcslTz.js → react-CYS24KmX.js} +1 -1
  77. package/dashboard/dist/assets/{search-BuBNF3E3.js → search-CUzE6zxr.js} +1 -1
  78. package/dashboard/dist/assets/{skills-api-DxAN5pNx.js → skills-api-D9SzxPFG.js} +1 -1
  79. package/dashboard/dist/assets/{sortable.esm-DoqMcEX-.js → sortable.esm-Bl2hAUyR.js} +1 -1
  80. package/dashboard/dist/assets/store-mHSJAJtE.js +1 -0
  81. package/dashboard/dist/assets/{terminal-COWFUJdU.js → terminal-BlQNEvn1.js} +1 -1
  82. package/dashboard/dist/assets/{trash-2-Cqjki3RI.js → trash-CHdgmOyk.js} +1 -1
  83. package/dashboard/dist/assets/{use-community-stats-DsrlOPDM.js → use-community-stats-DUiCs91N.js} +1 -1
  84. package/dashboard/dist/assets/{use-latest-request-guard-ryRFL2gx.js → use-latest-request-guard-ehXfkvuU.js} +1 -1
  85. package/dashboard/dist/assets/use-limits-display-prefs-CcwwQ0Fq.js +1 -0
  86. package/dashboard/dist/assets/{use-ordered-list-D-Q86R1b.js → use-ordered-list-CCOtI3Wa.js} +1 -1
  87. package/dashboard/dist/assets/use-reduced-motion-CDlahPqe.js +1 -0
  88. package/dashboard/dist/assets/{use-session-efficiency-pref-CrNz45y7.js → use-session-efficiency-pref-CYRDpcc_.js} +1 -1
  89. package/dashboard/dist/assets/{use-transform-CoGM21px.js → use-transform-btLlTJpj.js} +1 -1
  90. package/dashboard/dist/assets/use-usage-limits-BGr5sR-c.js +1 -0
  91. package/dashboard/dist/assets/{useCurrency-CgCfDktb.js → useCurrency-CNutuEKd.js} +1 -1
  92. package/dashboard/dist/assets/{useTokenFormat-QKtzCKTL.js → useTokenFormat-C0bAJFMC.js} +1 -1
  93. package/dashboard/dist/assets/{zap-CTIobzgU.js → zap-B2fbyjbc.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/devin.svg +6 -0
  97. package/dashboard/dist/index.html +20 -16
  98. package/dashboard/dist/ip-check.html +2 -2
  99. package/dashboard/dist/leaderboard.html +2 -2
  100. package/dashboard/dist/llms.txt +4 -2
  101. package/dashboard/dist/share.html +2 -2
  102. package/package.json +2 -2
  103. package/src/commands/init.js +114 -4
  104. package/src/commands/status.js +37 -0
  105. package/src/commands/sync.js +92 -10
  106. package/src/commands/uninstall.js +23 -0
  107. package/src/lib/codex-config.js +42 -0
  108. package/src/lib/devin-limits.js +249 -0
  109. package/src/lib/devin-usage.js +212 -0
  110. package/src/lib/local-api.js +19 -0
  111. package/src/lib/passive-mode.js +13 -1
  112. package/src/lib/pricing/curated-overrides.json +67 -1
  113. package/src/lib/pricing/index.js +5 -2
  114. package/src/lib/pricing/matcher.js +28 -0
  115. package/src/lib/pricing/seed-snapshot.json +1 -1
  116. package/src/lib/rollout.js +1310 -144
  117. package/src/lib/skills-manager.js +231 -30
  118. package/src/lib/subscriptions.js +70 -16
  119. package/src/lib/usage-limits.js +199 -49
  120. package/dashboard/dist/assets/DashboardPage-hV7wncKF.js +0 -1
  121. package/dashboard/dist/assets/DialogClose-C_dF-r3s.js +0 -1
  122. package/dashboard/dist/assets/DialogDescription-GClOSCaE.js +0 -1
  123. package/dashboard/dist/assets/DialogTitle-Buh_5688.js +0 -1
  124. package/dashboard/dist/assets/LimitsPage-_-BadDAj.js +0 -8
  125. package/dashboard/dist/assets/PetPage-Iq8Hswai.js +0 -1
  126. package/dashboard/dist/assets/PopoverPopup-DXr3JxVV.js +0 -1
  127. package/dashboard/dist/assets/ProviderIcon-sb2keSY2.js +0 -1
  128. package/dashboard/dist/assets/Select-vD-yj-qa.js +0 -1
  129. package/dashboard/dist/assets/SelectItemText-CRtFEuvg.js +0 -1
  130. package/dashboard/dist/assets/SettingsPage-CWVHrMer.js +0 -2
  131. package/dashboard/dist/assets/SkillsPage-DGWEH1xm.js +0 -1
  132. package/dashboard/dist/assets/ToolbarRootContext-D8yov1dW.js +0 -1
  133. package/dashboard/dist/assets/__vite-browser-external-_LUatzwU.js +0 -45
  134. package/dashboard/dist/assets/composite-BidfcMeb.js +0 -1
  135. package/dashboard/dist/assets/limits-providers-DRkcWQup.js +0 -1
  136. package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
  137. package/dashboard/dist/assets/provider-display-B7GggHQy.js +0 -42
  138. package/dashboard/dist/assets/store-Cg1HeUFM.js +0 -1
  139. package/dashboard/dist/assets/use-limits-display-prefs-DkSuu12m.js +0 -1
  140. package/dashboard/dist/assets/use-reduced-motion-D7NNqavo.js +0 -1
  141. package/dashboard/dist/assets/use-usage-limits-C_6L1sO5.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,
@@ -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 };
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+
3
+ // Devin CLI (Cognition — devin.ai) local history projection.
4
+ //
5
+ // Devin persists chat history in a SQLite database at
6
+ // `$XDG_DATA_HOME/devin/cli/sessions.db` (default
7
+ // `~/.local/share/devin/cli/sessions.db`). Token usage lives on assistant
8
+ // message nodes inside the `chat_message` JSON document. The SQL below is a
9
+ // deliberately narrow projection: request identity, generation model, the
10
+ // original generation timestamps, the numeric usage metrics, and the owning
11
+ // session's `working_directory`/`created_at` for canonical ordering and local
12
+ // project attribution. Message bodies, prompt text, node `metadata`,
13
+ // `sessions.cogs_json`, and any credential-bearing state are never selected.
14
+ //
15
+ // Verified against devin CLI 3000.10.21 (611c1cba): replay/fork/compaction
16
+ // produces multiple `message_nodes` rows that share one
17
+ // `metadata.request_id` with identical metrics and original timestamps, so
18
+ // `request_id` — never `row_id` or `(session_id, request_id)` — is the
19
+ // canonical usage identity across the whole local install.
20
+ //
21
+ // Token components are disjoint: the DB's `metrics.input_tokens` already
22
+ // excludes `cache_read_tokens` and `cache_creation_tokens`, so each reported
23
+ // counter is added once and caches are never subtracted from input.
24
+
25
+ const DEVIN_TABLE_PROBE_SQL =
26
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('message_nodes','sessions')";
27
+
28
+ const metricField = (jsonPath, alias) =>
29
+ `CASE WHEN json_valid(n.chat_message) THEN json_extract(n.chat_message, '${jsonPath}') ELSE NULL END AS ${alias}`;
30
+
31
+ // `sessions` is LEFT JOINed only for `working_directory` and `created_at`;
32
+ // older databases may not have the table, in which case the projection runs
33
+ // without it (project attribution and session-age ordering degrade to nulls).
34
+ function devinUsageSql({ hasSessionsTable = true } = {}) {
35
+ const join = hasSessionsTable ? "LEFT JOIN sessions s ON s.id = n.session_id" : "";
36
+ const sessionCreatedAt = hasSessionsTable ? "s.created_at" : "NULL";
37
+ const workingDirectory = hasSessionsTable ? "s.working_directory" : "NULL";
38
+ return `
39
+ SELECT
40
+ n.row_id AS row_id,
41
+ n.session_id AS session_id,
42
+ ${sessionCreatedAt} AS session_created_at,
43
+ ${workingDirectory} AS working_directory,
44
+ ${metricField("$.metadata.request_id", "request_id")},
45
+ ${metricField("$.metadata.generation_model", "generation_model")},
46
+ ${metricField("$.metadata.started_generation_at", "started_generation_at")},
47
+ ${metricField("$.metadata.created_at", "message_created_at")},
48
+ ${metricField("$.metadata.metrics.input_tokens", "input_tokens")},
49
+ ${metricField("$.metadata.metrics.output_tokens", "output_tokens")},
50
+ ${metricField("$.metadata.metrics.cache_read_tokens", "cache_read_tokens")},
51
+ ${metricField("$.metadata.metrics.cache_creation_tokens", "cache_creation_tokens")}
52
+ FROM message_nodes n
53
+ ${join}
54
+ WHERE json_valid(n.chat_message)
55
+ AND json_extract(n.chat_message, '$.role') = 'assistant'
56
+ AND json_extract(n.chat_message, '$.metadata.request_id') IS NOT NULL
57
+ ORDER BY n.row_id
58
+ `.trim();
59
+ }
60
+
61
+ function normalizeText(value) {
62
+ if (typeof value !== "string") return null;
63
+ const trimmed = value.trim();
64
+ return trimmed.length > 0 ? trimmed : null;
65
+ }
66
+
67
+ // Strict integer check: usage fields must be finite non-negative safe
68
+ // integers. `null`/`undefined` cache counters are legal (no reported cache
69
+ // contribution) — callers pass them through `optionalCount`; required
70
+ // counters go through `requiredCount`.
71
+ function toSafeCount(value) {
72
+ const n = Number(value);
73
+ return Number.isSafeInteger(n) && n >= 0 ? n : null;
74
+ }
75
+
76
+ function requiredCount(value) {
77
+ // A missing/NULL required counter is incomplete usage, not zero — pending
78
+ // assistant records carry no metrics block and must stay uncounted until
79
+ // the writer finalizes them.
80
+ if (value === null || value === undefined) return null;
81
+ return toSafeCount(value);
82
+ }
83
+
84
+ function optionalCount(value) {
85
+ if (value === null || value === undefined) return 0;
86
+ return toSafeCount(value);
87
+ }
88
+
89
+ // RFC3339 timestamps arrive with both `Z` and `+00:00` spellings; Date.parse
90
+ // normalizes both to the same instant. Returns epoch ms or 0 when missing or
91
+ // unparsable.
92
+ function parseDevinTimestampMs(value) {
93
+ if (typeof value !== "string" || !value.trim()) return 0;
94
+ const ms = Date.parse(value.trim());
95
+ return Number.isFinite(ms) && ms > 0 ? ms : 0;
96
+ }
97
+
98
+ // session.created_at is Unix seconds; node row ordering uses ms.
99
+ function epochSecondsToMs(value) {
100
+ const n = Number(value);
101
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) * 1000 : null;
102
+ }
103
+
104
+ // Normalize one projected row into a canonical candidate, or null when the
105
+ // record is pending/malformed: missing request identity, missing/invalid
106
+ // numeric usage, or no usable original timestamp. Such rows are re-evaluated
107
+ // on every scan, so a record that gains metrics or a timestamp later is
108
+ // counted then.
109
+ function normalizeDevinUsageRow(row) {
110
+ const requestId = normalizeText(row?.request_id);
111
+ if (!requestId) return null;
112
+
113
+ const input = requiredCount(row?.input_tokens);
114
+ const output = requiredCount(row?.output_tokens);
115
+ const cacheRead = optionalCount(row?.cache_read_tokens);
116
+ const cacheCreation = optionalCount(row?.cache_creation_tokens);
117
+ if (input === null || output === null || cacheRead === null || cacheCreation === null) {
118
+ return null;
119
+ }
120
+
121
+ // The original generation time decides the bucket; the node insertion time
122
+ // (n.created_at) is clone/replay insertion time and is never a fallback.
123
+ const tsMs =
124
+ parseDevinTimestampMs(row?.started_generation_at) ||
125
+ parseDevinTimestampMs(row?.message_created_at);
126
+ if (!tsMs) return null;
127
+
128
+ return {
129
+ requestId,
130
+ sessionId: normalizeText(row?.session_id) || "",
131
+ rowId: toSafeCount(row?.row_id) ?? 0,
132
+ sessionCreatedAtMs: epochSecondsToMs(row?.session_created_at),
133
+ workingDirectory: normalizeText(row?.working_directory),
134
+ model: normalizeText(row?.generation_model) || "unknown",
135
+ tsMs,
136
+ input,
137
+ output,
138
+ cacheRead,
139
+ cacheCreation,
140
+ };
141
+ }
142
+
143
+ // Pick the canonical retained record for one request_id. Copies created by
144
+ // replay/fork/compaction carry identical original timestamps and metrics, so
145
+ // the order is: earliest original generation time, then the earliest-created
146
+ // owning session (a fork's copy lives in a younger session), then session id,
147
+ // then newest row first so an in-place correction appended as a fresh node
148
+ // inside the same session wins over the stale sibling.
149
+ function compareCandidates(a, b) {
150
+ if (a.tsMs !== b.tsMs) return a.tsMs - b.tsMs;
151
+ const sessionA = a.sessionCreatedAtMs ?? Number.POSITIVE_INFINITY;
152
+ const sessionB = b.sessionCreatedAtMs ?? Number.POSITIVE_INFINITY;
153
+ if (sessionA !== sessionB) return sessionA - sessionB;
154
+ if (a.sessionId !== b.sessionId) return a.sessionId < b.sessionId ? -1 : 1;
155
+ return b.rowId - a.rowId;
156
+ }
157
+
158
+ // Build the deduplicated per-request event list. Conversation ownership and
159
+ // session/project attribution are decided by the persisted request ledger in
160
+ // parseDevinIncremental — the projection only reports each request's
161
+ // best-evidence retained record; `conversation_count` stays 0 here and is
162
+ // filled from ledger state at reconcile time.
163
+ function buildDevinUsageEvents(rows) {
164
+ const byRequest = new Map();
165
+ let rowsSeen = 0;
166
+ let rowsSkipped = 0;
167
+ for (const row of Array.isArray(rows) ? rows : []) {
168
+ rowsSeen += 1;
169
+ const candidate = normalizeDevinUsageRow(row);
170
+ if (!candidate) {
171
+ rowsSkipped += 1;
172
+ continue;
173
+ }
174
+ const list = byRequest.get(candidate.requestId);
175
+ if (list) list.push(candidate);
176
+ else byRequest.set(candidate.requestId, [candidate]);
177
+ }
178
+
179
+ const events = [];
180
+ for (const [requestId, candidates] of byRequest) {
181
+ candidates.sort(compareCandidates);
182
+ const canonical = candidates[0];
183
+ const total = canonical.input + canonical.output + canonical.cacheRead + canonical.cacheCreation;
184
+ events.push({
185
+ requestId,
186
+ sessionId: canonical.sessionId,
187
+ workingDirectory: canonical.workingDirectory,
188
+ model: canonical.model,
189
+ tsMs: canonical.tsMs,
190
+ totals: {
191
+ input_tokens: canonical.input,
192
+ cached_input_tokens: canonical.cacheRead,
193
+ cache_creation_input_tokens: canonical.cacheCreation,
194
+ output_tokens: canonical.output,
195
+ reasoning_output_tokens: 0,
196
+ total_tokens: total,
197
+ billable_total_tokens: total,
198
+ total_cost_usd: 0,
199
+ conversation_count: 0,
200
+ },
201
+ });
202
+ }
203
+ events.sort((a, b) => a.tsMs - b.tsMs || (a.requestId < b.requestId ? -1 : 1));
204
+
205
+ return { events, rowsSeen, rowsSkipped };
206
+ }
207
+
208
+ module.exports = {
209
+ DEVIN_TABLE_PROBE_SQL,
210
+ devinUsageSql,
211
+ buildDevinUsageEvents,
212
+ };
@@ -3160,6 +3160,11 @@ function createLocalApiHandler({ queuePath }) {
3160
3160
  json(res, { ok: true, skill: await skills.installSkill(body.skill, body.targets || ["claude", "codex"]) });
3161
3161
  return true;
3162
3162
  }
3163
+ if (action === "update_all") {
3164
+ // Partial success is normal, so this reports per-skill rather than failing.
3165
+ json(res, { ok: true, ...(await skills.updateSkills(Array.isArray(body.ids) ? body.ids : [])) });
3166
+ return true;
3167
+ }
3163
3168
  if (action === "uninstall") {
3164
3169
  json(res, { ok: true, ...(skills.uninstallSkill(body.id) || {}) });
3165
3170
  return true;
@@ -3215,6 +3220,19 @@ function createLocalApiHandler({ queuePath }) {
3215
3220
  if (p === "/functions/tokentracker-usage-limits") {
3216
3221
  const { getUsageLimits, resetUsageLimitsCache } = require("./usage-limits");
3217
3222
  try {
3223
+ // Devin quota is opt-in (Settings > Usage & Limits > Providers). An
3224
+ // explicit devin=1 without local authentication is rejected before any
3225
+ // cache reset or provider work — silently downgrading it to a disabled
3226
+ // response would misreport an enabled client as a disabled provider.
3227
+ // Authorization is evaluated unconditionally so the check never depends
3228
+ // on the user-controlled opt-in flag itself.
3229
+ const localAuthorized = isAuthorizedLocalMutation(req);
3230
+ const devinParam = url.searchParams.get("devin");
3231
+ const devinEnabled = devinParam === "1" || devinParam === "true";
3232
+ if (devinEnabled && !localAuthorized) {
3233
+ json(res, { error: "Unauthorized" }, 401);
3234
+ return true;
3235
+ }
3218
3236
  const refreshParam = url.searchParams.get("refresh");
3219
3237
  const forceRefresh = refreshParam === "1" || refreshParam === "true";
3220
3238
  if (forceRefresh) {
@@ -3227,6 +3245,7 @@ function createLocalApiHandler({ queuePath }) {
3227
3245
  // Punches through the Claude disk fresh-cache (but not the 429
3228
3246
  // cooldown) — an explicit user refresh should hit upstream.
3229
3247
  forceRefresh,
3248
+ devinEnabled,
3230
3249
  });
3231
3250
  json(res, data);
3232
3251
  } catch (e) {
@@ -89,7 +89,7 @@ function classifyWritableFailure(filePath) {
89
89
  * every_code, opencode, openclaw, codebuddy, workbuddy, grok }
90
90
  * @returns {PassiveProvider[]}
91
91
  */
92
- function detectPassiveProviders({ home, hookStatus }) {
92
+ function detectPassiveProviders({ home, hookStatus, env = process.env }) {
93
93
  const out = [];
94
94
 
95
95
  // Claude Code — logs at ~/.claude/projects/<name>/*.jsonl
@@ -128,6 +128,18 @@ function detectPassiveProviders({ home, hookStatus }) {
128
128
  settingsPath: path.join(home, ".codex", "config.toml"),
129
129
  }));
130
130
 
131
+ const acodeHome = env.TOKENTRACKER_ACODE_HOME || path.join(home, ".acode");
132
+ out.push(buildEntry({
133
+ name: "acode",
134
+ hookExpected: true,
135
+ hookInstalled: Boolean(hookStatus?.acode_notify ?? hookStatus?.acode),
136
+ logsDir: acodeHome,
137
+ logsPredicate: (_full, name, isDir) =>
138
+ (isDir && (name === "sessions" || name === "archived_sessions")) ||
139
+ (!isDir && name.endsWith(".jsonl")),
140
+ settingsPath: path.join(acodeHome, "config.toml"),
141
+ }));
142
+
131
143
  // Every Code — same shape as Codex, different home
132
144
  out.push(buildEntry({
133
145
  name: "every_code",