tokentracker-cli 1.5.6 → 1.5.7

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 (129) hide show
  1. package/README.md +2 -0
  2. package/dashboard/dist/assets/{AchievementBadge-NKpY___e.js → AchievementBadge-DIHKXKgm.js} +1 -1
  3. package/dashboard/dist/assets/{AchievementsPage-9nXmSX72.js → AchievementsPage-U66jjKFf.js} +2 -2
  4. package/dashboard/dist/assets/{AchievementsSection-DttQciSY.js → AchievementsSection-BAW79xKg.js} +1 -1
  5. package/dashboard/dist/assets/{BadgeDetailModal-bvWEdJbe.js → BadgeDetailModal-IfomOw9B.js} +1 -1
  6. package/dashboard/dist/assets/{BotAnimated-ChuUfph8.js → BotAnimated-B33gSUM3.js} +1 -1
  7. package/dashboard/dist/assets/{Card-SWtLDXt1.js → Card-dtrkef-y.js} +1 -1
  8. package/dashboard/dist/assets/{CloudCheckoutPage-c2fNl1Rl.js → CloudCheckoutPage-jYPgHJYE.js} +1 -1
  9. package/dashboard/dist/assets/{CloudPage-CfwDjZMS.js → CloudPage-BumdgM5w.js} +2 -2
  10. package/dashboard/dist/assets/{CommandPalette-tWgPa295.js → CommandPalette-CFEeoIo3.js} +1 -1
  11. package/dashboard/dist/assets/{CommunityStatsModal-D7UaaUdM.js → CommunityStatsModal-DL3MrPmb.js} +1 -1
  12. package/dashboard/dist/assets/{ConfirmModal-BrBcKhfW.js → ConfirmModal-BRdtCv2g.js} +1 -1
  13. package/dashboard/dist/assets/{Controls-CcvBkk7W.js → Controls-Bb-_ZAsT.js} +1 -1
  14. package/dashboard/dist/assets/DashboardPage-GXwt6DoC.js +1 -0
  15. package/dashboard/dist/assets/{DateRangePopover-BkN_pird.js → DateRangePopover-CHkFCFkE.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-BbUL7WwT.js → DevicePage-Da753sLS.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-BXO8SBFr.js → DialogClose-4A_jISLc.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-CiEhDvt9.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-Du332auj.js → DialogTitle-C0oQdI9n.js} +1 -1
  20. package/dashboard/dist/assets/{ElectricLogo-CXLYfOt7.js → ElectricLogo-Bv-sB-O-.js} +1 -1
  21. package/dashboard/dist/assets/{FadeIn-HSQBOc8B.js → FadeIn-Ch2AuGxY.js} +1 -1
  22. package/dashboard/dist/assets/{HeaderGithubStar-D5tyQDGE.js → HeaderGithubStar-C_dy8a5K.js} +1 -1
  23. package/dashboard/dist/assets/{HoverTooltip-BhidhbnD.js → HoverTooltip-iuzo5YeN.js} +1 -1
  24. package/dashboard/dist/assets/{IpCheckPage-WMEF1oS3.js → IpCheckPage-BKEID9fw.js} +1 -1
  25. package/dashboard/dist/assets/{LandingPage-BmO-59ZP.js → LandingPage-DZt5JWZ7.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardPage-C2WyniEt.js → LeaderboardPage-CYrCQF5P.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardProBadge-DAZnMRNn.js → LeaderboardProBadge-B8vG2SAW.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardProfileModal-D1BxcloV.js → LeaderboardProfileModal-DpAvBJPT.js} +3 -3
  29. package/dashboard/dist/assets/{LeaderboardProfilePage-RPJDTwpG.js → LeaderboardProfilePage-CofOk8GO.js} +1 -1
  30. package/dashboard/dist/assets/{LimitsPage-9ZOhKvn5.js → LimitsPage-DXknvxtI.js} +6 -6
  31. package/dashboard/dist/assets/{LocalOnlyNotice-CH6w4hRw.js → LocalOnlyNotice-BBVCEm5Q.js} +1 -1
  32. package/dashboard/dist/assets/{LoginCard-D24HsRjs.js → LoginCard-DcPazHo1.js} +1 -1
  33. package/dashboard/dist/assets/{LoginModal-sLvHyi9U.js → LoginModal-CGXPaeWD.js} +1 -1
  34. package/dashboard/dist/assets/{LoginPage-PIvLEw1Y.js → LoginPage-Ct6Al1pz.js} +1 -1
  35. package/dashboard/dist/assets/{PetPage-DYcw_4P1.js → PetPage-8NPEAa-c.js} +2 -2
  36. package/dashboard/dist/assets/{PopoverPopup-D7ZCWq5f.js → PopoverPopup-R69ks89h.js} +1 -1
  37. package/dashboard/dist/assets/{ProviderIcon-B0kB52K-.js → ProviderIcon-svF44YE0.js} +1 -1
  38. package/dashboard/dist/assets/{ResetPasswordPage-B_NT4Xfl.js → ResetPasswordPage-DJIYKu8C.js} +1 -1
  39. package/dashboard/dist/assets/{Select-BtxjjP2Q.js → Select-BVK4XZD5.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-Co1M6XRC.js → SelectItemText-Bh74D8lr.js} +1 -1
  41. package/dashboard/dist/assets/{SelfHostPage-XadfhFnd.js → SelfHostPage-8gj2lAFp.js} +1 -1
  42. package/dashboard/dist/assets/{ServiceStatusPage-ZEJyAxsQ.js → ServiceStatusPage-CWPV_Mou.js} +1 -1
  43. package/dashboard/dist/assets/SessionsPage-Bo5plK6O.js +3 -0
  44. package/dashboard/dist/assets/{SettingsPage-D4qHgIxn.js → SettingsPage-BptJrWn-.js} +3 -3
  45. package/dashboard/dist/assets/{SkillsPage-CkQCk2uF.js → SkillsPage-CRgGXnJS.js} +1 -1
  46. package/dashboard/dist/assets/{TokenGalaxy-Bx1vr4b0.js → TokenGalaxy-dgOy0Eo7.js} +1 -1
  47. package/dashboard/dist/assets/{TooltipPopup-DQW8FWJO.js → TooltipPopup-DpGnyoFz.js} +1 -1
  48. package/dashboard/dist/assets/{TrendMonitor-BMNcY3N8.js → TrendMonitor-BEy_GbMg.js} +1 -1
  49. package/dashboard/dist/assets/{WidgetsPage-luRTge7O.js → WidgetsPage-ChUelFrP.js} +1 -1
  50. package/dashboard/dist/assets/{WrappedPage-B1j3WgXx.js → WrappedPage-n63RsX8z.js} +1 -1
  51. package/dashboard/dist/assets/{__vite-browser-external-CvvpsW-U.js → __vite-browser-external-D_JnEwiu.js} +3 -3
  52. package/dashboard/dist/assets/{agent-logos-DL4fdks6.js → agent-logos-DQ1h5uQF.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-DiOsjgdu.js → arrow-left-Bo3Sg_cg.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-DBFXV7GU.js → arrow-right-DdxkL8Ul.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-BIvpDQHo.js → arrow-up-right-CpvelPD2.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days--psnXUPW.js → calendar-days-BvNVya0e.js} +1 -1
  57. package/dashboard/dist/assets/{check-DLqbsN00.js → check-tO40Glrg.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-C0P0oLzP.js → chevron-down-Dnrra3mC.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-NSmb7QNy.js → cloud-checkout-COC4zB8s.js} +1 -1
  60. package/dashboard/dist/assets/{composite-D7ahPHYQ.js → composite-C0NgnRTl.js} +1 -1
  61. package/dashboard/dist/assets/{copy-COD7vwd6.js → copy-DTKN2SO4.js} +1 -1
  62. package/dashboard/dist/assets/{download-CpSUX4U-.js → download-CSq2Yd3D.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-DXUjrooT.js → external-link-BtGedmuQ.js} +1 -1
  64. package/dashboard/dist/assets/{flame-Dl3ZT9oo.js → flame-ChjXIFSx.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-Ci49THbK.js → format-tokens-CXqZY114.js} +1 -1
  66. package/dashboard/dist/assets/{gift-DTHCN1Of.js → gift-YFwBvsUA.js} +1 -1
  67. package/dashboard/dist/assets/{icons-CB9s1e1k.js → icons-DCiBpbK0.js} +1 -1
  68. package/dashboard/dist/assets/{index-DFdfNKJM.js → index-B7sRX2EC.js} +1 -1
  69. package/dashboard/dist/assets/{index-BGT8ZQyn.js → index-DMqIbTL4.js} +1 -1
  70. package/dashboard/dist/assets/{index-DeX4t_5p.js → index-Dpmdg98T.js} +1 -1
  71. package/dashboard/dist/assets/{index-Dozwi-Se.js → index-XSya-vft.js} +1 -1
  72. package/dashboard/dist/assets/{info-CqM2wmSI.js → info-PYzcoobJ.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-Byo7Rj3t.js → link-2-BxSrdKyR.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-CMxd78Na.js → loader-circle-CgvCCDTH.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-CBjnoeck.js → local-usage-transfer-eTzMdhbs.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-Oowlk_FC.js → modifiers.esm-B796CRz-.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-B2EgB_9q.js → monitor-smartphone-M5nbcGRy.js} +1 -1
  78. package/dashboard/dist/assets/{play-BSuP-UBL.js → play-12ZzPH9r.js} +1 -1
  79. package/dashboard/dist/assets/{plus-BWemSIuC.js → plus-OqDA7xZy.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-BpzWuFDH.js → provider-display-JlEBv1VB.js} +1 -1
  81. package/dashboard/dist/assets/{react-cIJ4zx62.js → react-CeDcnQho.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-C_ZMtbgL.js → safePolygon-B_SGhQbo.js} +1 -1
  83. package/dashboard/dist/assets/{search-BK7eyNds.js → search-D9wz4rWp.js} +1 -1
  84. package/dashboard/dist/assets/{server-BqQHX-rd.js → server-UCMw4AQF.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-CO5U6gRw.js → shield-check-lJ0-Z5Ot.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-CMye76ag.js → skills-api-Da-WZbvW.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-Bvb61Lvj.js → sortable.esm-Bj1ZkbqW.js} +1 -1
  88. package/dashboard/dist/assets/{store-CICV1KXb.js → store-Bpwwxxsy.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-BKMWT1Ux.js → terminal-BHQMC4hm.js} +1 -1
  90. package/dashboard/dist/assets/{trash-D2Oqim4r.js → trash-yTV6vhGX.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-CEqwmjct.js → use-community-stats-rrE4Ohic.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-CTuzzu3F.js → use-latest-request-guard-D4Y1aCyd.js} +1 -1
  93. package/dashboard/dist/assets/{use-limits-display-prefs-CA2BLWkY.js → use-limits-display-prefs-ChHr4XEF.js} +1 -1
  94. package/dashboard/dist/assets/{use-native-settings-B54wHSj9.js → use-native-settings-BmoCls3D.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list--PU9ekxY.js → use-ordered-list-BZSSYcLx.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-CKTIXp4-.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-BRyR8szg.js → use-transform-CJO1b0Iu.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-sufKdfDY.js → use-usage-limits-BbAC_LWP.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-6oy7kSX8.js → useCurrency-B2enOMwJ.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-ZtDdVdJg.js → usePositioner-BaoW4QRL.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-tiOJ-PeE.js → useTokenFormat-B2bbeEu-.js} +1 -1
  102. package/dashboard/dist/assets/{zap-Dr1dXUjN.js → zap-CT5a8-_j.js} +1 -1
  103. package/dashboard/dist/index.html +1 -1
  104. package/dashboard/dist/ip-check.html +1 -1
  105. package/dashboard/dist/leaderboard.html +1 -1
  106. package/dashboard/dist/share.html +1 -1
  107. package/package.json +1 -1
  108. package/src/commands/init.js +9 -0
  109. package/src/commands/status.js +12 -6
  110. package/src/commands/sync.js +22 -4
  111. package/src/lib/claude-backend.js +291 -0
  112. package/src/lib/claude-config.js +1 -1
  113. package/src/lib/cursor-config.js +34 -9
  114. package/src/lib/cursor-safe-storage.js +119 -0
  115. package/src/lib/devin-limits.js +65 -25
  116. package/src/lib/diagnostics.js +3 -0
  117. package/src/lib/doctor.js +13 -0
  118. package/src/lib/local-api.js +2 -0
  119. package/src/lib/local-usage-transfer.js +22 -1
  120. package/src/lib/pricing/index.js +12 -0
  121. package/src/lib/pricing/seed-snapshot.json +1 -1
  122. package/src/lib/rollout.js +70 -15
  123. package/src/lib/session-analytics.js +20 -8
  124. package/src/lib/trae-cn-config.js +32 -2
  125. package/src/lib/usage-limits.js +601 -35
  126. package/dashboard/dist/assets/DashboardPage-DJSgRHGq.js +0 -1
  127. package/dashboard/dist/assets/DialogDescription-B1HyKP0x.js +0 -1
  128. package/dashboard/dist/assets/SessionsPage-Cte1taUu.js +0 -3
  129. package/dashboard/dist/assets/use-reduced-motion-CZ8_YWc2.js +0 -1
@@ -16,6 +16,7 @@
16
16
 
17
17
  const fs = require("node:fs");
18
18
  const path = require("node:path");
19
+ const { parse: parseToml } = require("smol-toml");
19
20
 
20
21
  const DEVIN_PLAN_STATUS_URL =
21
22
  "https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetPlanStatus";
@@ -28,25 +29,26 @@ function isNonEmptyString(value) {
28
29
  return typeof value === "string" && value.trim().length > 0;
29
30
  }
30
31
 
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;
32
+ // Local sign-in problems are persistent configuration errors, not transient
33
+ // fetch failures: the aggregator's disk-cache fallback must never mask them,
34
+ // so they carry a distinctive code the assembly can gate on.
35
+ function credentialsError(message) {
36
+ const error = new Error(message);
37
+ error.code = "CREDENTIALS_INVALID";
38
+ return error;
47
39
  }
48
40
 
49
- // Returns { apiKey } or null when no sign-in exists (no home, or the file is
41
+ // A credential-file revision, not a token fingerprint or account claim. Both
42
+ // in-place edits (ctime/mtime) and atomic replacements (device/inode) invalidate
43
+ // old windows, including across restarts. Never persist the token or its hash.
44
+ function devinCredentialIdentity(filePath, stat) {
45
+ return JSON.stringify([
46
+ path.resolve(filePath),
47
+ ...[stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].map(String),
48
+ ]);
49
+ }
50
+
51
+ // Returns { apiKey, cacheIdentity } or null when no sign-in exists (no home, or the file is
50
52
  // absent — ENOENT only). Any other read failure or a mangled file throws an
51
53
  // owned, actionable error with no path or filesystem details, so a broken
52
54
  // sign-in can never masquerade as "not configured".
@@ -64,20 +66,42 @@ function readDevinCredentials({ home, env = process.env } = {}) {
64
66
  ? path.join(dir, "devin", "credentials.toml")
65
67
  : path.join(dir, ".local", "share", "devin", "credentials.toml");
66
68
  let raw;
69
+ let descriptor;
70
+ let cacheIdentity = null;
67
71
  try {
68
- raw = fs.readFileSync(base, "utf8");
72
+ // Read the token and revision from the same opened file, so an atomic
73
+ // replacement cannot stamp the old token with the new file's revision.
74
+ descriptor = fs.openSync(base, "r");
75
+ const before = fs.fstatSync(descriptor, { bigint: true });
76
+ if (!before.isFile()) throw new Error("Not a credentials file");
77
+ raw = fs.readFileSync(descriptor, "utf8");
78
+ const after = fs.fstatSync(descriptor, { bigint: true });
79
+ const identity = devinCredentialIdentity(base, before);
80
+ if (identity === devinCredentialIdentity(base, after)) cacheIdentity = identity;
69
81
  } catch (error) {
70
82
  if (error && error.code === "ENOENT") return null;
71
- throw new Error(
83
+ throw credentialsError(
72
84
  "Could not read the Devin CLI credentials file — check its permissions or run `devin auth login` to sign in again.",
73
85
  );
86
+ } finally {
87
+ if (descriptor !== undefined) fs.closeSync(descriptor);
88
+ }
89
+ // Decode the credential document, then select only the root-level auth
90
+ // fields, like the other providers' structured credential readers.
91
+ let fields;
92
+ try {
93
+ fields = parseToml(raw);
94
+ } catch {
95
+ // Parser errors may quote credentials; expose only an owned message.
96
+ throw credentialsError(
97
+ "Could not parse the Devin CLI credentials file — run `devin auth login` to sign in again.",
98
+ );
74
99
  }
75
- const fields = parseDevinCredentialsToml(raw);
76
100
  const apiKey = isNonEmptyString(fields.windsurf_api_key)
77
101
  ? fields.windsurf_api_key.trim()
78
102
  : null;
79
103
  if (!apiKey) {
80
- throw new Error(
104
+ throw credentialsError(
81
105
  "Devin credentials file has no sign-in token — run `devin auth login`.",
82
106
  );
83
107
  }
@@ -85,11 +109,20 @@ function readDevinCredentials({ home, env = process.env } = {}) {
85
109
  ? fields.api_server_url.trim().replace(/\/+$/, "")
86
110
  : null;
87
111
  if (apiServerUrl && apiServerUrl !== DEVIN_API_ORIGIN) {
88
- throw new Error(
112
+ throw credentialsError(
89
113
  "Devin credentials point at a custom api_server_url, which this quota provider does not support.",
90
114
  );
91
115
  }
92
- return { apiKey };
116
+ return { apiKey, cacheIdentity };
117
+ }
118
+
119
+ function resolveDevinCacheIdentity(options) {
120
+ try {
121
+ return readDevinCredentials(options)?.cacheIdentity || null;
122
+ } catch {
123
+ // Unreadable, missing or invalid current credentials must fail closed.
124
+ return null;
125
+ }
93
126
  }
94
127
 
95
128
  function malformed(field) {
@@ -226,9 +259,14 @@ async function fetchDevinLimits({ home, env = process.env, enabled = false, fetc
226
259
  throw error;
227
260
  }
228
261
  if (!response?.ok) {
229
- throw new Error(
262
+ const error = new Error(
230
263
  `Devin quota request was rejected (HTTP ${response?.status ?? "?"}).`,
231
264
  );
265
+ // The aggregate's disk-cache fallback only covers transient failures:
266
+ // carrying the status lets it tell a 429/5xx blip from an ambiguous or
267
+ // persistent rejection like HTTP 400 (docs/devin-limits.md).
268
+ if (typeof response?.status === "number") error.statusCode = response.status;
269
+ throw error;
232
270
  }
233
271
  let body;
234
272
  try {
@@ -243,7 +281,9 @@ async function fetchDevinLimits({ home, env = process.env, enabled = false, fetc
243
281
  plan_label: windows.plan_label,
244
282
  primary_window: windows.primary_window,
245
283
  secondary_window: windows.secondary_window,
284
+ // Internal: the aggregate strips this before returning its payload.
285
+ cache_identity: credentials.cacheIdentity,
246
286
  };
247
287
  }
248
288
 
249
- module.exports = { fetchDevinLimits };
289
+ module.exports = { fetchDevinLimits, resolveDevinCacheIdentity };
@@ -20,6 +20,7 @@ const { probeOpenclawSessionPluginState } = require("./openclaw-session-plugin")
20
20
  const { probeGrokHookState } = require("./grok-hook");
21
21
  const { resolveTrackerPaths } = require("./tracker-paths");
22
22
  const { resolveDeviceTokenPresence } = require("./cloud-device-token");
23
+ const { inspectTraeCnAuth } = require("./trae-cn-config");
23
24
  const wsl = require("./wsl-probe");
24
25
  // TASK-011: Kiro paths inlined here to avoid pulling the ~4000-line
25
26
  // rollout module on every `tokentracker status` / `diagnostics` call.
@@ -195,6 +196,7 @@ async function collectTrackerDiagnostics({
195
196
  ? new Date(uploadThrottle.lastSuccessMs).toISOString()
196
197
  : null;
197
198
  const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
199
+ const traeCn = inspectTraeCnAuth({ home });
198
200
 
199
201
  return {
200
202
  ok: true,
@@ -233,6 +235,7 @@ async function collectTrackerDiagnostics({
233
235
  merge_policy:
234
236
  "Kiro IDE and Kiro CLI both emit source='kiro' in queue.jsonl so token, cost, heatmap, and leaderboard aggregations merge transparently. Use this block to distinguish sub-path contributions.",
235
237
  },
238
+ trae_cn: { ...traeCn, storage_path: redactValue(traeCn.storage_path, home) },
236
239
  scan_roots: {
237
240
  codex: describeScanRoots(scanRoots.codex),
238
241
  claude: describeScanRoots(scanRoots.claude),
package/src/lib/doctor.js CHANGED
@@ -293,6 +293,19 @@ async function checkNetwork({ baseUrl, fetch }) {
293
293
 
294
294
  function buildDiagnosticsChecks(diagnostics) {
295
295
  const checks = [];
296
+ const traeCn = diagnostics?.trae_cn;
297
+ if (traeCn) {
298
+ const { TRAE_CN_AUTH_HELP } = require("./trae-cn-config");
299
+ checks.push({
300
+ id: "trae_cn.auth",
301
+ status: !traeCn.usage_opt_in || traeCn.auth === "readable" ? "ok" : "warn",
302
+ detail: !traeCn.usage_opt_in
303
+ ? "usage sync off (TOKENTRACKER_TRAE_CN_USAGE=1 opts in); auth not read; no TRAE request made"
304
+ : `local auth ${traeCn.auth}; API access not checked (${traeCn.storage_path || "storage.json not found"}). ${TRAE_CN_AUTH_HELP}`,
305
+ critical: false,
306
+ meta: traeCn,
307
+ });
308
+ }
296
309
  const notify = diagnostics?.notify || {};
297
310
  const notifyConfigured = Boolean(
298
311
  notify.codex_notify_configured ||
@@ -1,3 +1,4 @@
1
+ const { hasUnknownBackendCost } = require("./claude-backend");
1
2
  const fs = require("node:fs");
2
3
  const os = require("node:os");
3
4
  const path = require("node:path");
@@ -2942,6 +2943,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2942
2943
  if (!sa.models.has(mdl))
2943
2944
  sa.models.set(mdl, { model: mdl, model_id: mdl, pricing: getModelPricingInfo(mdl, { source: src }), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0, total_cost_usd: "0" } });
2944
2945
  const ma = sa.models.get(mdl);
2946
+ if (hasUnknownBackendCost(row)) ma.pricing = getModelPricingInfo(mdl, row);
2945
2947
  const reportedCost = ["grok", "cline"].includes(src) && Number(row.total_cost_usd) > 0;
2946
2948
  const costSource = reportedCost ? "provider_reported" : "model_pricing";
2947
2949
  ma.cost_source = ma.cost_source && ma.cost_source !== costSource ? "mixed" : costSource;
@@ -7,6 +7,8 @@ const crypto = require("node:crypto");
7
7
  const { openLock, writeFileAtomic } = require("./fs");
8
8
  const { isAccountLevelSource } = require("./source-metadata");
9
9
 
10
+ const { backendFromUrl, TOKEN_FIELDS: BACKEND_TOKEN_FIELDS } = require("./claude-backend");
11
+
10
12
  const SCHEMA = "tokentracker.local-usage.v1";
11
13
  const MAX_BYTES = 5 * 1024 * 1024;
12
14
  const MAX_STATE_BYTES = 64 * 1024 * 1024;
@@ -15,7 +17,7 @@ const TOKEN_FIELDS = ["input_tokens", "output_tokens", "cached_input_tokens", "c
15
17
  "reasoning_output_tokens", "total_tokens", "billable_total_tokens", "conversation_count",
16
18
  ...["long_context_", "priority_", "priority_long_context_", "ultrafast_", "ultrafast_long_context_"].flatMap((prefix) =>
17
19
  ["input_tokens", "output_tokens", "cached_input_tokens", "cache_creation_input_tokens", "reasoning_output_tokens"].map((field) => prefix + field))];
18
- const ROW_FIELDS = new Set(["source", "model", "hour_start", "total_cost_usd", ...TOKEN_FIELDS]);
20
+ const ROW_FIELDS = new Set(["source", "model", "hour_start", "total_cost_usd", "backend_usage", ...TOKEN_FIELDS]);
19
21
  const validId = (id) => typeof id === "string" && /^[A-Za-z0-9_-]{8,128}$/.test(id);
20
22
  const validTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
21
23
  const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
@@ -47,6 +49,25 @@ function cleanRow(row, strict = false) {
47
49
  if (!Number.isFinite(row.total_cost_usd) || row.total_cost_usd < 0) fail("invalid_snapshot");
48
50
  result.total_cost_usd = row.total_cost_usd;
49
51
  }
52
+ if (row.backend_usage !== undefined) {
53
+ if (row.source !== "claude" || !Array.isArray(row.backend_usage)) fail("invalid_snapshot");
54
+ const sums = {};
55
+ result.backend_usage = row.backend_usage.map((part) => {
56
+ if (!part || typeof part !== "object") fail("invalid_snapshot");
57
+ const identity = backendFromUrl(part.backend_origin);
58
+ if (part.backend !== identity.backend || part.backend_origin !== identity.backend_origin ||
59
+ Object.keys(part).some((key) => !["backend", "backend_origin", ...BACKEND_TOKEN_FIELDS].includes(key))) fail("invalid_snapshot");
60
+ const clean = { ...identity };
61
+ for (const field of BACKEND_TOKEN_FIELDS) {
62
+ const value = part[field] ?? 0;
63
+ if (!Number.isSafeInteger(value) || value < 0) fail("invalid_snapshot");
64
+ sums[field] = (sums[field] || 0) + value;
65
+ if (sums[field] > (result[field] || 0)) fail("invalid_snapshot");
66
+ clean[field] = value;
67
+ }
68
+ return clean;
69
+ });
70
+ }
50
71
  return result;
51
72
  }
52
73
 
@@ -1,3 +1,4 @@
1
+ const { hasUnknownBackendCost, TOKEN_FIELDS } = require("../claude-backend");
1
2
  // Public pricing API. Replaces the hard-coded MODEL_PRICING table that used
2
3
  // to live in src/lib/local-api.js. Keeps the same synchronous shape so all
3
4
  // existing callers (computeRowCost, /functions/* handlers, tests) work
@@ -139,6 +140,7 @@ const pricingInfoCache = { revision: -1, entries: new Map() };
139
140
  // Preserve matcher provenance: a known zero rate is free, a missing model is
140
141
  // unpriced. Both return zero dollars, but they mean different things in the UI.
141
142
  function getModelPricingInfo(model, opts = {}) {
143
+ if (hasUnknownBackendCost(opts)) return { status: "unpriced", source: null, ...ZERO_PRICING };
142
144
  const source = typeof opts === "string" ? opts : opts.source;
143
145
  const sourceKey = typeof source === "string" ? source.toLowerCase() : null;
144
146
  if (pricingInfoCache.revision !== state.revision) {
@@ -340,6 +342,16 @@ function speedTierSubsetCost(row, prefix, pricing, ctx) {
340
342
  // computeRowCost in src/lib/local-api.js. Moved here so vite mock + local
341
343
  // server share one source of truth.
342
344
  function computeRowCost(row) {
345
+ if (hasUnknownBackendCost(row)) {
346
+ // Existing totals without provenance retain their historical estimate. Only
347
+ // the newly ingested custom/unknown subset is excluded from model pricing.
348
+ const priced = { ...row, backend_usage: undefined };
349
+ for (const part of row.backend_usage) {
350
+ if (part.backend === "anthropic") continue;
351
+ for (const field of TOKEN_FIELDS) priced[field] = Math.max(0, (priced[field] || 0) - (part[field] || 0));
352
+ }
353
+ return computeRowCost(priced);
354
+ }
343
355
  if (LOCAL_INFERENCE_SOURCES.has(String(row?.source || "").toLowerCase())) return 0;
344
356
  // Pi can route a turn through a subscription-backed Copilot provider. Pi's
345
357
  // usage record reports a zero marginal cost for those turns; do not