tokentracker-cli 0.91.0 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.de.md +7 -5
  2. package/README.ja.md +9 -5
  3. package/README.ko.md +9 -5
  4. package/README.md +7 -5
  5. package/README.zh-CN.md +6 -4
  6. package/dashboard/dist/assets/{AchievementBadge-iyRg0rYF.js → AchievementBadge-o8hl3my2.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CkBm3bN3.js → AchievementsPage-CjDEyZfD.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-a4Z7VtlP.js → AchievementsSection-Cwv1Zmyf.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-DHTN0Ky6.js → BadgeDetailModal-BDxTgOvT.js} +1 -1
  10. package/dashboard/dist/assets/{Card-CdygGEqz.js → Card-Cp5v7Y19.js} +1 -1
  11. package/dashboard/dist/assets/{ClawdAnimated-D-Z823Nc.js → ClawdAnimated-6Ibb4qCZ.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-VGkj8w2X.js → CommandPalette-CIf9oAqq.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-BDT4xqfF.js → CommunityStatsModal-UwiOPFu0.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-BIKv4AlL.js → ConfirmModal-xkYsrh0v.js} +1 -1
  15. package/dashboard/dist/assets/{Controls-BkZVm9t1.js → Controls-C2Z2X0ZQ.js} +1 -1
  16. package/dashboard/dist/assets/{DashboardPage-BFkdjUdd.js → DashboardPage-Do_NMUFK.js} +1 -1
  17. package/dashboard/dist/assets/{DevicePage-BGw861bu.js → DevicePage-DL5OjugJ.js} +1 -1
  18. package/dashboard/dist/assets/{DialogClose-CSEl6ru8.js → DialogClose-D0w08oR2.js} +1 -1
  19. package/dashboard/dist/assets/DialogDescription-rhGVm7N2.js +1 -0
  20. package/dashboard/dist/assets/{DialogTitle-CWPdu5V-.js → DialogTitle-IKAPm5Pk.js} +1 -1
  21. package/dashboard/dist/assets/{FadeIn-CdCANtvs.js → FadeIn-Dve77aEj.js} +1 -1
  22. package/dashboard/dist/assets/{HeaderGithubStar-dQQnH85f.js → HeaderGithubStar-BVYbLWDi.js} +1 -1
  23. package/dashboard/dist/assets/{HoverTooltip-CqJUI-WG.js → HoverTooltip-BrF9-z7f.js} +1 -1
  24. package/dashboard/dist/assets/{IpCheckPage-BRqTXlSu.js → IpCheckPage-BZ1VyVsm.js} +1 -1
  25. package/dashboard/dist/assets/{LandingPage-DhMTgDng.js → LandingPage-jkjtQl58.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardAvatar-BrGcByG1.js → LeaderboardAvatar-D9kUudjQ.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardPage-BmdopPjo.js → LeaderboardPage-DXg7Qv8T.js} +2 -2
  28. package/dashboard/dist/assets/{LeaderboardProfileModal-DRZqPo-A.js → LeaderboardProfileModal-jTlaOi-j.js} +4 -4
  29. package/dashboard/dist/assets/{LeaderboardProfilePage-CTbC39KX.js → LeaderboardProfilePage-DsKQ_Of8.js} +3 -3
  30. package/dashboard/dist/assets/{LimitsPage-B7AuLq1S.js → LimitsPage-CMqZAQCa.js} +1 -1
  31. package/dashboard/dist/assets/{LocalOnlyNotice-DyIAspoq.js → LocalOnlyNotice-CkLs3CF_.js} +1 -1
  32. package/dashboard/dist/assets/LoginCard-SmOXM_PX.js +1 -0
  33. package/dashboard/dist/assets/{LoginModal-D3bmr_jI.js → LoginModal-CvtEBkrO.js} +1 -1
  34. package/dashboard/dist/assets/{LoginPage-D7SgmEhZ.js → LoginPage-Zm-dRt_Y.js} +1 -1
  35. package/dashboard/dist/assets/{PetPage-5A3Q0zlw.js → PetPage-Bcj7hzNz.js} +1 -1
  36. package/dashboard/dist/assets/{PopoverPopup-ajEhdI8m.js → PopoverPopup-CnteSROW.js} +1 -1
  37. package/dashboard/dist/assets/ProviderIcon-DcQmGbfz.js +1 -0
  38. package/dashboard/dist/assets/{ResetPasswordPage-Bwbt8tH2.js → ResetPasswordPage-zMb_m3Cx.js} +1 -1
  39. package/dashboard/dist/assets/{SegmentedControl-BSa5hMnN.js → SegmentedControl-D3KkbQ7T.js} +1 -1
  40. package/dashboard/dist/assets/{Select-kPKDGb7M.js → Select-0RiFEISo.js} +1 -1
  41. package/dashboard/dist/assets/{SelectItemText-DNgLclTV.js → SelectItemText-BRV3DFEB.js} +1 -1
  42. package/dashboard/dist/assets/{ServiceStatusPage-C1j7rPDp.js → ServiceStatusPage-CWF_vKOC.js} +1 -1
  43. package/dashboard/dist/assets/{SessionsPage-BgoKs_Yq.js → SessionsPage-DIqoT7MU.js} +2 -2
  44. package/dashboard/dist/assets/{SettingsPage-BUaPftV0.js → SettingsPage-DQZIqiE5.js} +2 -2
  45. package/dashboard/dist/assets/{SkillsPage-C5GkzfWW.js → SkillsPage-BIIOUHnq.js} +1 -1
  46. package/dashboard/dist/assets/{TokenGalaxy-DZffAa_0.js → TokenGalaxy-Bulaqjbm.js} +1 -1
  47. package/dashboard/dist/assets/{ToolbarRootContext-CuLP92P_.js → ToolbarRootContext-Dg-zL8fE.js} +1 -1
  48. package/dashboard/dist/assets/TrendMonitor-DfafDX6b.js +19 -0
  49. package/dashboard/dist/assets/{WidgetsPage-BUhiCpcq.js → WidgetsPage-F_2yLE7n.js} +1 -1
  50. package/dashboard/dist/assets/{WrappedPage-sNFISXVG.js → WrappedPage-BFxYKGQs.js} +1 -1
  51. package/dashboard/dist/assets/{__vite-browser-external-Cyw7ZNtj.js → __vite-browser-external-CZMdPW0g.js} +4 -3
  52. package/dashboard/dist/assets/agent-logos-DjyCpabA.js +1 -0
  53. package/dashboard/dist/assets/{arrow-up-right-BcRkt7hr.js → arrow-up-right-CIAd6sXA.js} +1 -1
  54. package/dashboard/dist/assets/{check-qhr4U2S5.js → check-Bt3Yu07t.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-iLGEAP7_.js → chevron-right-BiXULxma.js} +1 -1
  56. package/dashboard/dist/assets/{copy-BhEPCq-q.js → copy-D3k1Xbfl.js} +1 -1
  57. package/dashboard/dist/assets/{download-D7O1ZzsX.js → download-co2WHJvo.js} +1 -1
  58. package/dashboard/dist/assets/{external-link-CY2kK2hC.js → external-link-D5az2QwL.js} +1 -1
  59. package/dashboard/dist/assets/{flame-D4FPuBNn.js → flame-CSCUyn3Z.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-CVHJ7Hdh.js → format-tokens-BebRLYzr.js} +1 -1
  61. package/dashboard/dist/assets/{icons-B_-51UXa.js → icons-C_y5qr6T.js} +1 -1
  62. package/dashboard/dist/assets/{index-ClVWat-1.js → index-CXOOOeSl.js} +1 -1
  63. package/dashboard/dist/assets/{index-cwdr25Zm.js → index-DP3XlgDm.js} +1 -1
  64. package/dashboard/dist/assets/{index-Djv-aJy0.js → index-R488h3mD.js} +1 -1
  65. package/dashboard/dist/assets/{info-DOab0eZz.js → info-CD_G_DZp.js} +1 -1
  66. package/dashboard/dist/assets/{limits-providers-CESm2d6B.js → limits-providers-BlAlATd7.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-B4LMsC4m.js → link-2-DIn-EC5g.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-CPx6wpSn.js → loader-circle-B5Az_VQe.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-C9z8mbdn.js → maximize-2-ptnkCbEN.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-5xRgAl_R.js → modifiers.esm-DEN3VPKe.js} +1 -1
  71. package/dashboard/dist/assets/module-CV1mYFsv.js +3 -0
  72. package/dashboard/dist/assets/{monitor-smartphone-B9HAspdR.js → monitor-smartphone-Dqr_CnOQ.js} +1 -1
  73. package/dashboard/dist/assets/provider-display-BOAoggwg.js +42 -0
  74. package/dashboard/dist/assets/{react-C45OGvDd.js → react-B3eL33ha.js} +1 -1
  75. package/dashboard/dist/assets/{search-Bg1qyEdz.js → search-CNNSHW9P.js} +1 -1
  76. package/dashboard/dist/assets/{skills-api-a8pi7UTd.js → skills-api-BShLlV2m.js} +1 -1
  77. package/dashboard/dist/assets/{sortable.esm-CtPp8cFA.js → sortable.esm-C6kDS9DZ.js} +1 -1
  78. package/dashboard/dist/assets/{store-b8zXM1yr.js → store-B4atnDzu.js} +1 -1
  79. package/dashboard/dist/assets/{terminal-CPf0K0Fn.js → terminal-CXMttuiJ.js} +1 -1
  80. package/dashboard/dist/assets/{trash-2-uorzBdA0.js → trash-2-aXKBjf5M.js} +1 -1
  81. package/dashboard/dist/assets/{use-community-stats-DaVwywsM.js → use-community-stats-rGR2J-Vi.js} +1 -1
  82. package/dashboard/dist/assets/{use-limits-display-prefs-UIzz7PJ3.js → use-limits-display-prefs-CazeqdNI.js} +1 -1
  83. package/dashboard/dist/assets/{use-ordered-list-C2XOfMH3.js → use-ordered-list-Bx-enbl8.js} +1 -1
  84. package/dashboard/dist/assets/use-reduced-motion-2NcjtyzY.js +1 -0
  85. package/dashboard/dist/assets/{use-session-efficiency-pref-D5KHakYg.js → use-session-efficiency-pref-B7RHUtup.js} +1 -1
  86. package/dashboard/dist/assets/{use-transform-Cfo_2_id.js → use-transform-CWVOY76a.js} +1 -1
  87. package/dashboard/dist/assets/{use-usage-limits-CANTpxrA.js → use-usage-limits-C9XRiYNi.js} +1 -1
  88. package/dashboard/dist/assets/{useCurrency-CjvWekKH.js → useCurrency-D7ICju5j.js} +1 -1
  89. package/dashboard/dist/assets/{useScrollLock-ChQdbuJg.js → useScrollLock-DXWPQF_D.js} +1 -1
  90. package/dashboard/dist/assets/{useTokenFormat-4MY04JPl.js → useTokenFormat-wDhHrsAL.js} +1 -1
  91. package/dashboard/dist/assets/{zap-BSXHFUBb.js → zap-BZDqgZR1.js} +1 -1
  92. package/dashboard/dist/brand-logos/dots.png +0 -0
  93. package/dashboard/dist/brand-logos/pi.svg +8 -15
  94. package/dashboard/dist/index.html +31 -26
  95. package/dashboard/dist/ip-check.html +1 -1
  96. package/dashboard/dist/leaderboard.html +1 -1
  97. package/dashboard/dist/llms.txt +4 -2
  98. package/dashboard/dist/share.html +1 -1
  99. package/package.json +2 -2
  100. package/src/commands/init.js +3 -1
  101. package/src/commands/status.js +46 -0
  102. package/src/commands/sync.js +188 -4
  103. package/src/lib/account-usage-dedup.js +267 -0
  104. package/src/lib/local-api.js +4 -0
  105. package/src/lib/pricing/curated-overrides.json +15 -9
  106. package/src/lib/pricing/index.js +32 -1
  107. package/src/lib/pricing/seed-snapshot.json +1 -1
  108. package/src/lib/rollout.js +730 -1
  109. package/src/lib/source-metadata.js +1 -1
  110. package/src/lib/trae-cn-config.js +539 -0
  111. package/dashboard/dist/assets/DialogDescription-DfNXnqay.js +0 -1
  112. package/dashboard/dist/assets/LoginCard-LcOaKaCK.js +0 -1
  113. package/dashboard/dist/assets/ProviderIcon-C4yuu9FG.js +0 -1
  114. package/dashboard/dist/assets/TrendMonitor-BKLAKJmT.js +0 -19
  115. package/dashboard/dist/assets/agent-logos-Bwf2YFHS.js +0 -1
  116. package/dashboard/dist/assets/module-BRJxSJmz.js +0 -3
  117. package/dashboard/dist/assets/provider-display-C0NS0EvN.js +0 -42
  118. package/dashboard/dist/assets/use-reduced-motion-CNyMWBog.js +0 -1
@@ -165,7 +165,7 @@
165
165
  ]
166
166
  }
167
167
  </script>
168
- <script type="module" crossorigin src="/assets/__vite-browser-external-Cyw7ZNtj.js"></script>
168
+ <script type="module" crossorigin src="/assets/__vite-browser-external-CZMdPW0g.js"></script>
169
169
  <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-CRYifSM4.css">
170
170
  </head>
171
171
  <body>
@@ -136,7 +136,7 @@
136
136
  ]
137
137
  }
138
138
  </script>
139
- <script type="module" crossorigin src="/assets/__vite-browser-external-Cyw7ZNtj.js"></script>
139
+ <script type="module" crossorigin src="/assets/__vite-browser-external-CZMdPW0g.js"></script>
140
140
  <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-CRYifSM4.css">
141
141
  </head>
142
142
  <body>
@@ -1,7 +1,7 @@
1
1
  # Token Tracker LLMs Index
2
2
 
3
3
  Token Tracker is a free, open-source, local-first AI coding usage and cost
4
- tracker for 32 tools. Its distinctive desktop experience includes a pixel pet
4
+ tracker for 34 tools. Its distinctive desktop experience includes a pixel pet
5
5
  powered by real token activity, four desktop widgets, and achievements. The
6
6
  same data powers a cross-platform CLI, a native macOS menu bar app, a native
7
7
  Windows system tray app, and a dashboard. It records token counts and
@@ -23,7 +23,7 @@ timestamps, never prompts, responses, or source code.
23
23
  - [npm package](https://www.npmjs.com/package/tokentracker-cli)
24
24
  - [Latest release](https://github.com/xiufengsun/TokenTracker/releases/latest)
25
25
 
26
- ## Supported AI coding tools (32)
26
+ ## Supported AI coding tools (34)
27
27
  - Claude Code
28
28
  - Codex CLI
29
29
  - Cursor
@@ -41,6 +41,7 @@ timestamps, never prompts, responses, or source code.
41
41
  - Grok Build
42
42
  - oh-my-pi
43
43
  - pi
44
+ - Dots
44
45
  - Prime Agent
45
46
  - Craft Agents
46
47
  - Reasonix
@@ -56,6 +57,7 @@ timestamps, never prompts, responses, or source code.
56
57
  - AnythingLLM Desktop
57
58
  - Claude Science
58
59
  - DeepSeek Harness
60
+ - TRAE Work CN
59
61
 
60
62
  ## Capabilities
61
63
  - Desktop pet that reacts to real token burn, streaks, and rest
@@ -75,7 +75,7 @@
75
75
  "description": "Shareable Token Tracker dashboard snapshot."
76
76
  }
77
77
  </script>
78
- <script type="module" crossorigin src="/assets/__vite-browser-external-Cyw7ZNtj.js"></script>
78
+ <script type="module" crossorigin src="/assets/__vite-browser-external-CZMdPW0g.js"></script>
79
79
  <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-CRYifSM4.css">
80
80
  </head>
81
81
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.91.0",
4
- "description": "Local-first AI coding token usage and cost tracker for 32 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
3
+ "version": "0.92.0",
4
+ "description": "Local-first AI coding token usage and cost tracker for 34 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
7
7
  "tokentracker-cli": "bin/tracker.js",
@@ -103,7 +103,7 @@ const DIVIDER = "----------------------------------------------";
103
103
  const DEFAULT_DASHBOARD_URL = "https://www.tokentracker.cc";
104
104
 
105
105
  // Single source of truth for the welcome screen's provider count + sample list.
106
- // test/discovery-metadata.test.js keeps this aligned with public 27-tool copy.
106
+ // test/discovery-metadata.test.js keeps this aligned with public 32-tool copy.
107
107
  const SUPPORTED_PROVIDERS = [
108
108
  "Claude Code",
109
109
  "Codex CLI",
@@ -122,6 +122,7 @@ const SUPPORTED_PROVIDERS = [
122
122
  "Grok Build",
123
123
  "oh-my-pi",
124
124
  "pi",
125
+ "Dots",
125
126
  "Prime Agent",
126
127
  "Craft Agents",
127
128
  "Reasonix",
@@ -137,6 +138,7 @@ const SUPPORTED_PROVIDERS = [
137
138
  "AnythingLLM Desktop",
138
139
  "Claude Science",
139
140
  "DeepSeek Harness",
141
+ "TRAE Work CN",
140
142
  ];
141
143
 
142
144
  async function cmdInit(argv) {
@@ -89,6 +89,13 @@ const {
89
89
  resolveCopilotAppDbPaths,
90
90
  probeWslDistros,
91
91
  } = require("../lib/rollout");
92
+ const {
93
+ TRAE_CN_USAGE_ENV,
94
+ isTraeCnUsageEnabled,
95
+ resolveTraeCnStoragePath,
96
+ readTraeCnAuthFromStorage,
97
+ extractTraeCnToken,
98
+ } = require("../lib/trae-cn-config");
92
99
  const wsl = require("../lib/wsl-probe");
93
100
  const { getWslMode, isInvalidWslMode, shouldProbeWsl, discoverWslHome } = wsl;
94
101
  const { resolveInstallPaths, resolveZcodeNativeDbPath } = require("../lib/install-resolver");
@@ -642,6 +649,34 @@ async function cmdStatus(argv = []) {
642
649
  ? readTraeEntitlementFromStorage(traeStoragePath)
643
650
  : null;
644
651
 
652
+ // Trae SOLO CN — opt-in usage-API reader. Unlike the passive entitlement
653
+ // reader above, this source sends the locally stored sign-in JWT to TRAE's
654
+ // official API, so status surfaces the opt-in flag, the resolved storage
655
+ // path, and whether the auth blob decrypts — the three inputs a "why is my
656
+ // TRAE CN data missing" diagnosis needs.
657
+ // A resolvable default path does NOT mean the app is installed - the macOS
658
+ // resolver always derives one. Match the sync path semantics exactly:
659
+ // installed iff the storage file actually exists on disk.
660
+ const traeCnStoragePath = resolveTraeCnStoragePath({ env: process.env, home });
661
+ const traeCnInstalled = Boolean(
662
+ traeCnStoragePath && fssync.existsSync(traeCnStoragePath),
663
+ );
664
+ let traeCnAuthState = "not-signed-in";
665
+ if (traeCnInstalled) {
666
+ try {
667
+ const traeCnAuth = readTraeCnAuthFromStorage({ env: process.env, home, platform: process.platform });
668
+ if (traeCnAuth) {
669
+ extractTraeCnToken(traeCnAuth);
670
+ traeCnAuthState = "readable";
671
+ }
672
+ } catch (error) {
673
+ // Distinguish a real IO failure (unreadable) from present-but-bad
674
+ // data (malformed); neither ever carries storage contents.
675
+ traeCnAuthState = error?.code === "TRAE_CN_STORAGE_UNREADABLE" ? "unreadable" : "malformed";
676
+ }
677
+ }
678
+ const traeCnUsageOptIn = isTraeCnUsageEnabled(process.env);
679
+
645
680
  // Grok Build (xAI TUI)
646
681
  const grokHookState = await probeGrokHookState({ home, trackerDir, env: process.env });
647
682
  const grokSessions = grokHookState.hasGrokInstall || grokHookState.sessionsDir
@@ -933,6 +968,14 @@ async function cmdStatus(argv = []) {
933
968
  ...(traeEntitlement ? { entitlement: traeEntitlement } : {}),
934
969
  }
935
970
  : { installed: false },
971
+ "trae-cn": traeCnInstalled
972
+ ? {
973
+ installed: true,
974
+ detail: traeCnStoragePath,
975
+ auth: traeCnAuthState,
976
+ usage_opt_in: traeCnUsageOptIn,
977
+ }
978
+ : { installed: false },
936
979
  grok_build: grokInstalled
937
980
  ? {
938
981
  installed: true,
@@ -1105,6 +1148,9 @@ async function cmdStatus(argv = []) {
1105
1148
  traeEntitlement
1106
1149
  ? `- Trae SOLO plan: ${formatTraeEntitlementLine(traeEntitlement)}`
1107
1150
  : null,
1151
+ traeCnInstalled
1152
+ ? `- Trae SOLO CN: usage sync ${traeCnUsageOptIn ? "opted in" : `off (set ${TRAE_CN_USAGE_ENV}=1 to enable)`}, auth ${traeCnAuthState} (${traeCnStoragePath})`
1153
+ : null,
1108
1154
  ...(() => {
1109
1155
  if (!hermesInstalled) return [];
1110
1156
  return [`- Hermes Agent: state.db found (${hermesPath})`];
@@ -112,6 +112,7 @@ const {
112
112
  resolveDroidModel,
113
113
  resolveDshSessionFiles,
114
114
  parseDshIncremental,
115
+ parseTraeCnApiIncremental,
115
116
  bucketKey,
116
117
  toUtcHalfHourStart,
117
118
  totalsKey,
@@ -134,6 +135,11 @@ const {
134
135
  parseCursorCsv,
135
136
  } = require("../lib/cursor-config");
136
137
  const { purgeProjectUsage } = require("../lib/project-usage-purge");
138
+ const {
139
+ fetchTraeCnUsageWithAuth,
140
+ resolveTraeCnStoragePath,
141
+ isTraeCnUsageEnabled,
142
+ } = require("../lib/trae-cn-config");
137
143
  const {
138
144
  isCodexSessionCursorPath,
139
145
  isCursorStoreRetry,
@@ -291,6 +297,7 @@ const AUTO_SYNC_SOURCES = new Set([
291
297
  "qoder-cn",
292
298
  "reasonix",
293
299
  "roocode",
300
+ "trae-cn",
294
301
  "workbuddy",
295
302
  "zcode",
296
303
  "zed",
@@ -452,6 +459,15 @@ async function cmdSync(argv, context = {}) {
452
459
  const lockWaitOptions = context && typeof context === "object"
453
460
  ? context.lockWaitOptions
454
461
  : undefined;
462
+ // Narrow test-only seam for TRAE Work CN: deterministic injected fetch + a
463
+ // fixed "now" for the rolling range. Production keeps the global fetch and
464
+ // current time; no public CLI flag/config is introduced.
465
+ const traeCnFetchImpl = context && typeof context === "object"
466
+ ? context.traeCnFetchImpl
467
+ : undefined;
468
+ const traeCnNowMs = context && typeof context === "object"
469
+ ? context.traeCnNowMs
470
+ : undefined;
455
471
  const syncDiagnostics = diagnostics && typeof diagnostics === "object" ? diagnostics : null;
456
472
  const home = os.homedir();
457
473
  const { trackerDir } = await resolveTrackerPaths({ home });
@@ -1663,6 +1679,93 @@ async function cmdSync(argv, context = {}) {
1663
1679
  }
1664
1680
  }
1665
1681
 
1682
+ // ── Trae Work CN (国内版) — account-level usage API ──
1683
+ // A simple explicit rolling 30-day window captured once per sync (no
1684
+ // cursor checkpoints / full-history import / page retries / background
1685
+ // requests). Runs only when the user has opted in via
1686
+ // TOKENTRACKER_TRAE_CN_USAGE=1 (the read transmits the locally stored
1687
+ // sign-in JWT to TRAE's official endpoint, so it is never default-on),
1688
+ // the sync is non-lightweight, the source is allowed, and the CN storage
1689
+ // file exists — which excludes both ordinary background and `--auto
1690
+ // --background --all-local-sources`. Fetching goes through the
1691
+ // storage-backed helper so its single 401/403 reread/retry is used; an
1692
+ // empty response is a successful no-op; each window is bounded to 100
1693
+ // pages/2,000 rows and an over-capacity window is split into staggered
1694
+ // sub-windows (still over capacity at the finest allowed granularity, or
1695
+ // any other fetch/page/schema/auth failure) skips the parser and leaves
1696
+ // prior data untouched while unrelated providers continue.
1697
+ let traeCnResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1698
+ if (
1699
+ !isBackgroundLightweightSync &&
1700
+ isTraeCnUsageEnabled(process.env) &&
1701
+ sourceAllowed("trae-cn")
1702
+ ) {
1703
+ const nowMs = Number.isFinite(traeCnNowMs) ? traeCnNowMs : Date.now();
1704
+ const fetchImpl = typeof traeCnFetchImpl === "function" ? traeCnFetchImpl : fetch;
1705
+ const endTime = Math.floor(nowMs / 1000);
1706
+ // Align the REAL fetch start down to a half-hour boundary so the API
1707
+ // range fully contains every bucket it can touch (a raw 08:37 start
1708
+ // would only partially cover the 08:30 bucket) - session bucket floors
1709
+ // never straddle the queried range.
1710
+ const HALF_HOUR_SEC = 30 * 60;
1711
+ const startTime = Math.max(
1712
+ 1,
1713
+ Math.floor((endTime - 30 * 24 * 60 * 60) / HALF_HOUR_SEC) * HALF_HOUR_SEC,
1714
+ );
1715
+ try {
1716
+ // Absent storage means "not signed in" — a silent skip, not an error.
1717
+ const traeCnStoragePath = resolveTraeCnStoragePath({ env: process.env, home });
1718
+ if (traeCnStoragePath && fssync.existsSync(traeCnStoragePath)) {
1719
+ if (progress?.enabled) {
1720
+ progress.start(`Fetching TRAE Work CN usage...`);
1721
+ }
1722
+ const traeCnUsage = await fetchTraeCnUsageWithAuth({
1723
+ start_time: startTime,
1724
+ end_time: endTime,
1725
+ fetchImpl,
1726
+ env: process.env,
1727
+ home,
1728
+ });
1729
+ const traeCnSessions = Array.isArray(traeCnUsage?.sessions) ? traeCnUsage.sessions : [];
1730
+ // An empty payload parses as a pure no-op: the TRAE absence
1731
+ // contract is NOT PROVEN (no evidence that a missing session means
1732
+ // deleted/zero), so an empty response asserts nothing - no usage
1733
+ // mutation and no session states. Non-empty snapshots append one
1734
+ // canonical session-state observation per CHANGED session (the
1735
+ // cloud LWW upsert reconciles cross-device versions).
1736
+ {
1737
+ if (progress?.enabled && traeCnSessions.length > 0) {
1738
+ progress.start(
1739
+ `Parsing TRAE Work CN ${renderBar(0)} 0/${formatNumber(
1740
+ traeCnSessions.length,
1741
+ )} records | buckets 0`,
1742
+ );
1743
+ }
1744
+ traeCnResult = await parseTraeCnApiIncremental({
1745
+ sessions: traeCnSessions,
1746
+ cursors,
1747
+ queuePath,
1748
+ onProgress: makeProviderProgress("TRAE Work CN"),
1749
+ windowStartMs: startTime * 1000,
1750
+ windowEndMs: endTime * 1000,
1751
+ // The LOGICAL fetch stamp carried into every session state's
1752
+ // snapshot_verified_at: the same clock that produced the query
1753
+ // range, not the later enqueue moment (append-only queue
1754
+ // replays it verbatim, so transport retries never fake
1755
+ // freshness).
1756
+ snapshotVerifiedAtMs: nowMs,
1757
+ });
1758
+ // A partially malformed snapshot throws inside the parser (fail
1759
+ // closed - it must not become the authoritative window state);
1760
+ // warnProviderParseFailure below reports it without sensitive
1761
+ // data while unrelated providers continue.
1762
+ }
1763
+ }
1764
+ } catch (err) {
1765
+ warnProviderParseFailure("TRAE Work CN", err, opts);
1766
+ }
1767
+ }
1768
+
1666
1769
  // ── Kiro (SQLite-based, with JSONL fallback; dual-install aware) ──
1667
1770
  let kiroResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1668
1771
  if (sourceAllowed("kiro")) {
@@ -2157,7 +2260,12 @@ async function cmdSync(argv, context = {}) {
2157
2260
  }
2158
2261
 
2159
2262
  // ── Grok Build (xAI) ──
2160
- let grokResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2263
+ let grokResult = {
2264
+ recordsProcessed: 0,
2265
+ eventsAggregated: 0,
2266
+ bucketsQueued: 0,
2267
+ projectBucketsQueued: 0,
2268
+ };
2161
2269
  // Full passive scan of all Grok sessions (historical + any not covered by hook)
2162
2270
  const grokSessions = sourceAllowed("grok") ? resolveGrokBuildSessions(process.env) : [];
2163
2271
  const grokSessionInputs = [...grokSessions];
@@ -2179,6 +2287,11 @@ async function cmdSync(argv, context = {}) {
2179
2287
  sessionId: hookSessionId,
2180
2288
  sessionDir:
2181
2289
  typeof grokHookSignal.sessionDir === "string" ? grokHookSignal.sessionDir : undefined,
2290
+ cwd: typeof grokHookSignal.cwd === "string" ? grokHookSignal.cwd : undefined,
2291
+ encodedCwd:
2292
+ typeof grokHookSignal.sessionDir === "string" && grokHookSignal.sessionDir
2293
+ ? path.basename(path.dirname(grokHookSignal.sessionDir))
2294
+ : undefined,
2182
2295
  updatesPath:
2183
2296
  typeof grokHookSignal.updatesPath === "string" ? grokHookSignal.updatesPath : undefined,
2184
2297
  signalsPath:
@@ -2208,6 +2321,7 @@ async function cmdSync(argv, context = {}) {
2208
2321
  sessions: grokSessionInputs,
2209
2322
  cursors,
2210
2323
  queuePath,
2324
+ projectQueuePath,
2211
2325
  env: process.env,
2212
2326
  onProgress: (p) => {
2213
2327
  if (!progress?.enabled) return;
@@ -2224,6 +2338,8 @@ async function cmdSync(argv, context = {}) {
2224
2338
  recordsProcessed: grokResult.recordsProcessed + grokScanResult.recordsProcessed,
2225
2339
  eventsAggregated: grokResult.eventsAggregated + grokScanResult.eventsAggregated,
2226
2340
  bucketsQueued: grokResult.bucketsQueued + grokScanResult.bucketsQueued,
2341
+ projectBucketsQueued:
2342
+ (grokResult.projectBucketsQueued || 0) + (grokScanResult.projectBucketsQueued || 0),
2227
2343
  };
2228
2344
  }
2229
2345
  if (isFullSourceScan && opts.repairGrok) {
@@ -2584,6 +2700,7 @@ async function cmdSync(argv, context = {}) {
2584
2700
  qoderCnResult.recordsProcessed +
2585
2701
  claudeScienceResult.recordsProcessed +
2586
2702
  cursorResult.recordsProcessed +
2703
+ traeCnResult.recordsProcessed +
2587
2704
  kiroResult.recordsProcessed +
2588
2705
  kiroCliResult.recordsProcessed +
2589
2706
  hermesResult.recordsProcessed +
@@ -2619,6 +2736,7 @@ async function cmdSync(argv, context = {}) {
2619
2736
  qoderCnResult.bucketsQueued +
2620
2737
  claudeScienceResult.bucketsQueued +
2621
2738
  cursorResult.bucketsQueued +
2739
+ traeCnResult.bucketsQueued +
2622
2740
  kiroResult.bucketsQueued +
2623
2741
  kiroCliResult.bucketsQueued +
2624
2742
  hermesResult.bucketsQueued +
@@ -2650,6 +2768,7 @@ async function cmdSync(argv, context = {}) {
2650
2768
  cursorStore.requiresCommit !== true &&
2651
2769
  totalParsed === 0 &&
2652
2770
  totalBuckets === 0 &&
2771
+ !(grokResult.projectBucketsQueued > 0) &&
2653
2772
  !codexColdAuditDue &&
2654
2773
  !codexFallbackRetryRan &&
2655
2774
  !grokHookSignalConsumed &&
@@ -2972,6 +3091,8 @@ function recordCodexColdScanAudit(
2972
3091
  module.exports = {
2973
3092
  cmdSync,
2974
3093
  acquireSyncLock,
3094
+ readQueueBatch,
3095
+ drainQueueToCloud,
2975
3096
  migrateCursorUnknownBuckets,
2976
3097
  migrateRolloutCumulativeDeltaBuckets,
2977
3098
  repairCodebuddyLogJsonlOverlap,
@@ -3472,7 +3593,10 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
3472
3593
  for (let batch = 0; batch < maxBatches; batch++) {
3473
3594
  if (offset >= queueSize) break;
3474
3595
  const result = await readQueueBatch(queuePath, offset, limit);
3475
- if (result.buckets.length === 0) break;
3596
+ // A states-only tail (e.g. a retry batch whose bucket rows already
3597
+ // uploaded) is still a meaningful upload: it advances the canonical
3598
+ // session corrections cloud-side.
3599
+ if (result.buckets.length === 0 && result.sessionStates.length === 0) break;
3476
3600
 
3477
3601
  const root = baseUrl.replace(/\/$/, "");
3478
3602
  const anonKey = process.env.TOKENTRACKER_INSFORGE_ANON_KEY || "";
@@ -3485,7 +3609,12 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
3485
3609
  const res = await fetch(`${root}/functions/${INGEST_SLUG}`, {
3486
3610
  method: "POST",
3487
3611
  headers,
3488
- body: JSON.stringify({ hourly: result.buckets }),
3612
+ body: JSON.stringify({
3613
+ hourly: result.buckets,
3614
+ ...(result.sessionStates.length > 0
3615
+ ? { account_session_states: result.sessionStates }
3616
+ : {}),
3617
+ }),
3489
3618
  });
3490
3619
 
3491
3620
  const rawText = await res.text().catch(() => "");
@@ -3522,6 +3651,26 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
3522
3651
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
3523
3652
 
3524
3653
  const bucketMap = new Map();
3654
+ // Account session states (kind: "account_session_state") are queue
3655
+ // control records, not usage rows: collect them separately so they ride
3656
+ // the same upload (after the bucket rows in file order) without entering
3657
+ // the bucket stream. Keyed by (source, session_id) LAST-WINS: the
3658
+ // append-only queue writes a session's newer observation after its older
3659
+ // one, and only the latest per session may reach the ingest batch (a
3660
+ // duplicate would make the cloud ON CONFLICT affect one row twice).
3661
+ // snapshot_verified_at rides along verbatim from the ORIGINAL fetch, so a
3662
+ // transport retry never fakes logical freshness.
3663
+ //
3664
+ // Batching: session-state records count toward the SAME per-batch record
3665
+ // cap (maxBuckets) as bucket rows. The ingest edge rejects a request with
3666
+ // more than 500 states (HTTP 400); before they counted, a states-heavy
3667
+ // queue (a fresh device's first 30-day TRAE sync appends one record per
3668
+ // observed session) was read in ONE uncapped batch - the 400 left the
3669
+ // queue offset untouched and every retry re-read the identical oversized
3670
+ // request, a permanent upload failure. With the default batchSize of 200
3671
+ // (and the MAX_INGEST_BUCKETS=500 clamp above), every request stays under
3672
+ // the edge cap while batching follows the existing design.
3673
+ const sessionStateMap = new Map();
3525
3674
  let offset = startOffset;
3526
3675
  let linesRead = 0;
3527
3676
  for await (const line of rl) {
@@ -3534,6 +3683,37 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
3534
3683
  } catch (_e) {
3535
3684
  continue;
3536
3685
  }
3686
+ if (bucket?.kind === "account_session_state") {
3687
+ if (typeof bucket.source === "string" && typeof bucket.session_id === "string" && bucket.session_id.trim()) {
3688
+ const ssSource = bucket.source.trim().toLowerCase();
3689
+ sessionStateMap.set(ssSource + "|" + bucket.session_id.trim(), {
3690
+ source: ssSource,
3691
+ session_id: bucket.session_id.trim(),
3692
+ model: typeof bucket.model === "string" ? bucket.model : "",
3693
+ bucket_start: typeof bucket.bucket_start === "string" ? bucket.bucket_start : "",
3694
+ input_tokens: Number(bucket.input_tokens) || 0,
3695
+ output_tokens: Number(bucket.output_tokens) || 0,
3696
+ cached_input_tokens: Number(bucket.cached_input_tokens) || 0,
3697
+ cache_creation_input_tokens: Number(bucket.cache_creation_input_tokens) || 0,
3698
+ reasoning_output_tokens: Number(bucket.reasoning_output_tokens) || 0,
3699
+ total_tokens: Number(bucket.total_tokens) || 0,
3700
+ ...(typeof bucket.snapshot_verified_at === "string" && bucket.snapshot_verified_at
3701
+ ? { snapshot_verified_at: bucket.snapshot_verified_at }
3702
+ : {}),
3703
+ });
3704
+ // Counted like a bucket row: the record rides THIS batch and its
3705
+ // bytes are covered by nextOffset, so the next batch resumes right
3706
+ // after it.
3707
+ linesRead += 1;
3708
+ if (linesRead >= maxBuckets) break;
3709
+ }
3710
+ continue;
3711
+ }
3712
+ // Pre-release watermark records from this feature branch's dev queues
3713
+ // are obsolete control records: drop them instead of letting them enter
3714
+ // the bucket stream (they carry no hour_start and would be skipped
3715
+ // downstream anyway, but be explicit).
3716
+ if (bucket?.kind === "account_sync_watermark") continue;
3537
3717
  const hourStart = typeof bucket?.hour_start === "string" ? bucket.hour_start : null;
3538
3718
  if (!hourStart) continue;
3539
3719
  const source = (typeof bucket?.source === "string" ? bucket.source.trim().toLowerCase() : "") || "codex";
@@ -3553,7 +3733,11 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
3553
3733
 
3554
3734
  rl.close();
3555
3735
  stream.close?.();
3556
- return { buckets: Array.from(bucketMap.values()), nextOffset: offset };
3736
+ return {
3737
+ buckets: Array.from(bucketMap.values()),
3738
+ sessionStates: Array.from(sessionStateMap.values()),
3739
+ nextOffset: offset,
3740
+ };
3557
3741
  }
3558
3742
 
3559
3743
  function normalizeGrokRepairSource(value) {