tokentracker-cli 0.90.2 → 0.91.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) 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-BJl0ibLg.js → AchievementBadge-LHOuekQ3.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CV6aNe3g.js → AchievementsPage-DNzeYWBl.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DxLHu9EV.js → AchievementsSection-Ch5dImt8.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-ConoHhIL.js → BadgeDetailModal-CuikWkUy.js} +1 -1
  10. package/dashboard/dist/assets/{Card-C_VQxG6G.js → Card-Chwjmxjz.js} +1 -1
  11. package/dashboard/dist/assets/{ClawdAnimated-_Mfn4MnU.js → ClawdAnimated-BdB7aycS.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-CCRCnYY9.js → CommandPalette-BMwuSEWM.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-DuICwpdM.js → CommunityStatsModal-eEDF-xn8.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-CI27robt.js → ConfirmModal-Be7f6XdL.js} +1 -1
  15. package/dashboard/dist/assets/{Controls-Cwu0gtgj.js → Controls-CtBFPCBY.js} +1 -1
  16. package/dashboard/dist/assets/{DashboardPage-GsFvPie1.js → DashboardPage-M0p-SU-f.js} +1 -1
  17. package/dashboard/dist/assets/{DevicePage-BSK76UQE.js → DevicePage-CAfcUond.js} +1 -1
  18. package/dashboard/dist/assets/{DialogClose-BXQlPs02.js → DialogClose-kd1wIljN.js} +1 -1
  19. package/dashboard/dist/assets/DialogDescription-CjJLpfCL.js +1 -0
  20. package/dashboard/dist/assets/{DialogTitle-DyvhHV1-.js → DialogTitle-DhB-tbnZ.js} +1 -1
  21. package/dashboard/dist/assets/{FadeIn-DaP6F4f8.js → FadeIn-B8eRWBdy.js} +1 -1
  22. package/dashboard/dist/assets/{HeaderGithubStar-CKB5puCd.js → HeaderGithubStar-DmWep8uP.js} +1 -1
  23. package/dashboard/dist/assets/{HoverTooltip-BG6k61dF.js → HoverTooltip-DDyTZli0.js} +1 -1
  24. package/dashboard/dist/assets/{IpCheckPage-Oqt1J9S2.js → IpCheckPage-CN8hYm4o.js} +1 -1
  25. package/dashboard/dist/assets/{LandingPage-45qoUn79.js → LandingPage-B3cHASqK.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardAvatar-ZgXnaI_N.js → LeaderboardAvatar-CEcLXrJL.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardPage-C7E_LrFk.js → LeaderboardPage-H6HW9hon.js} +2 -2
  28. package/dashboard/dist/assets/{LeaderboardProfileModal-D6PUcj3b.js → LeaderboardProfileModal-B_5PyjRA.js} +4 -4
  29. package/dashboard/dist/assets/{LeaderboardProfilePage-VcC9DK_B.js → LeaderboardProfilePage-DR9KWLWv.js} +3 -3
  30. package/dashboard/dist/assets/{LimitsPage-B9K0RFQD.js → LimitsPage-Da0hRxO_.js} +1 -1
  31. package/dashboard/dist/assets/{LocalOnlyNotice-BGsfKEM9.js → LocalOnlyNotice-DuTdva_n.js} +1 -1
  32. package/dashboard/dist/assets/LoginCard-DOL3osrj.js +1 -0
  33. package/dashboard/dist/assets/{LoginModal-N3pWGciC.js → LoginModal-DBmaQL2L.js} +1 -1
  34. package/dashboard/dist/assets/{LoginPage-TGgL0aXF.js → LoginPage-Dk08VrkC.js} +1 -1
  35. package/dashboard/dist/assets/{PetPage-DPi7OT_4.js → PetPage-BbCIdpzx.js} +1 -1
  36. package/dashboard/dist/assets/{PopoverPopup-BkptZaPd.js → PopoverPopup-C1fJZ4qt.js} +1 -1
  37. package/dashboard/dist/assets/ProviderIcon-FfLD-f73.js +1 -0
  38. package/dashboard/dist/assets/{ResetPasswordPage-DQ4qdjBD.js → ResetPasswordPage-Bg7eVc6L.js} +1 -1
  39. package/dashboard/dist/assets/{SegmentedControl-CuTrxHXS.js → SegmentedControl-CFNHcUz7.js} +1 -1
  40. package/dashboard/dist/assets/{Select-_Txi2j1D.js → Select-B0Q0eDcJ.js} +1 -1
  41. package/dashboard/dist/assets/{SelectItemText-DVKhj-YN.js → SelectItemText-BVbLYxSQ.js} +1 -1
  42. package/dashboard/dist/assets/{ServiceStatusPage-ChflNSK0.js → ServiceStatusPage-DCZH2kz5.js} +1 -1
  43. package/dashboard/dist/assets/{SessionsPage-CHv6xK2J.js → SessionsPage-DO5Et7ss.js} +2 -2
  44. package/dashboard/dist/assets/{SettingsPage-DkWAYALG.js → SettingsPage-afgxGfzT.js} +2 -2
  45. package/dashboard/dist/assets/{SkillsPage-DnTSlBl-.js → SkillsPage-DDKiHCF7.js} +1 -1
  46. package/dashboard/dist/assets/{TokenGalaxy-yAMCaU-k.js → TokenGalaxy-DV1Xoe5z.js} +1 -1
  47. package/dashboard/dist/assets/{ToolbarRootContext-BeDIqB0k.js → ToolbarRootContext-DNckd9Qz.js} +1 -1
  48. package/dashboard/dist/assets/TrendMonitor-CliMJxCC.js +19 -0
  49. package/dashboard/dist/assets/{WidgetsPage-y_qKZ9Gv.js → WidgetsPage-BrwG6YMM.js} +1 -1
  50. package/dashboard/dist/assets/{WrappedPage-BGUGI6vk.js → WrappedPage-Ft9YJd0D.js} +1 -1
  51. package/dashboard/dist/assets/{__vite-browser-external-CRvi_9hb.js → __vite-browser-external-C6_BtvDo.js} +4 -3
  52. package/dashboard/dist/assets/agent-logos-yYNu1ZUy.js +1 -0
  53. package/dashboard/dist/assets/{arrow-up-right-CjEesyJI.js → arrow-up-right-B_K57Vbg.js} +1 -1
  54. package/dashboard/dist/assets/{check-KDrH1gwE.js → check-xH63o2xJ.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-DseqFm-D.js → chevron-right-D3F2rAfr.js} +1 -1
  56. package/dashboard/dist/assets/{copy-CFJ1QKAQ.js → copy-WPfJGGI0.js} +1 -1
  57. package/dashboard/dist/assets/{download-BQUNGUdh.js → download-BgtWtEXI.js} +1 -1
  58. package/dashboard/dist/assets/{external-link-BCEOLLH2.js → external-link-DSHGoZRM.js} +1 -1
  59. package/dashboard/dist/assets/{flame-BigtoGg2.js → flame-CNGl6Qac.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-CS1GUpAV.js → format-tokens-jVm2UX7Y.js} +1 -1
  61. package/dashboard/dist/assets/{icons-DpZ1a8Ct.js → icons-B1JZg-Id.js} +1 -1
  62. package/dashboard/dist/assets/{index-DqkwD6eO.js → index-BTZibjLi.js} +1 -1
  63. package/dashboard/dist/assets/{index-lGAAw1Rv.js → index-BjD81Qce.js} +1 -1
  64. package/dashboard/dist/assets/{index-DFU_yVc5.js → index-CUFHknAy.js} +1 -1
  65. package/dashboard/dist/assets/{info-Br_jtGtO.js → info-CLM460KM.js} +1 -1
  66. package/dashboard/dist/assets/{limits-providers-CkqO1LMQ.js → limits-providers-CFO4O1qh.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-D-nAhkk-.js → link-2-BHOsUnAI.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-CuNPuDRR.js → loader-circle-CgcIQi92.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-dlvwitfg.js → maximize-2-DvqfuJEA.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-ORha_lPI.js → modifiers.esm-CNiwIkPh.js} +1 -1
  71. package/dashboard/dist/assets/module-CV1mYFsv.js +3 -0
  72. package/dashboard/dist/assets/{monitor-smartphone-CI2qvlMB.js → monitor-smartphone-BLutpxrG.js} +1 -1
  73. package/dashboard/dist/assets/provider-display-BxIgWUEa.js +42 -0
  74. package/dashboard/dist/assets/{react-Dy9iWvqT.js → react-CM-ozVnh.js} +1 -1
  75. package/dashboard/dist/assets/{search-Do7tzc3k.js → search-4T07INKt.js} +1 -1
  76. package/dashboard/dist/assets/{skills-api-CJ-8EFac.js → skills-api-EXMD3LD1.js} +1 -1
  77. package/dashboard/dist/assets/{sortable.esm-BKQg8NHC.js → sortable.esm-jC6mPJiL.js} +1 -1
  78. package/dashboard/dist/assets/{store-SUT-n3k3.js → store-CvSEdgf2.js} +1 -1
  79. package/dashboard/dist/assets/{terminal-BX4DOxd6.js → terminal-D9fyE2pP.js} +1 -1
  80. package/dashboard/dist/assets/{trash-2-VERlG247.js → trash-2-QLUsAY3l.js} +1 -1
  81. package/dashboard/dist/assets/{use-community-stats-BX6_XUdu.js → use-community-stats-Chrt3kpU.js} +1 -1
  82. package/dashboard/dist/assets/{use-limits-display-prefs-BpC3tYRw.js → use-limits-display-prefs-BJJd750e.js} +1 -1
  83. package/dashboard/dist/assets/{use-ordered-list-CX-LREPo.js → use-ordered-list-BBJp5lFv.js} +1 -1
  84. package/dashboard/dist/assets/use-reduced-motion-BvXZCGFC.js +1 -0
  85. package/dashboard/dist/assets/{use-session-efficiency-pref-B1G8hzVM.js → use-session-efficiency-pref-BXbvx2Ht.js} +1 -1
  86. package/dashboard/dist/assets/{use-transform-DNCqLbUD.js → use-transform-BVp_MUsU.js} +1 -1
  87. package/dashboard/dist/assets/{use-usage-limits-Cvbvf30m.js → use-usage-limits-Dk3EkeIs.js} +1 -1
  88. package/dashboard/dist/assets/{useCurrency-EciQmJkm.js → useCurrency-Bi88Sd__.js} +1 -1
  89. package/dashboard/dist/assets/{useScrollLock-sLkmam8C.js → useScrollLock-if34g2H-.js} +1 -1
  90. package/dashboard/dist/assets/{useTokenFormat-nb9na-2-.js → useTokenFormat-C2cz-5qJ.js} +1 -1
  91. package/dashboard/dist/assets/{zap-BQOWCMov.js → zap-D1yHmcQR.js} +1 -1
  92. package/dashboard/dist/index.html +28 -24
  93. package/dashboard/dist/ip-check.html +1 -1
  94. package/dashboard/dist/leaderboard.html +1 -1
  95. package/dashboard/dist/llms.txt +4 -2
  96. package/dashboard/dist/share.html +1 -1
  97. package/package.json +2 -2
  98. package/src/commands/init.js +12 -1
  99. package/src/commands/status.js +59 -0
  100. package/src/commands/sync.js +220 -4
  101. package/src/lib/account-usage-dedup.js +267 -0
  102. package/src/lib/local-api.js +4 -0
  103. package/src/lib/pricing/curated-overrides.json +15 -9
  104. package/src/lib/pricing/index.js +38 -2
  105. package/src/lib/pricing/matcher.js +1 -0
  106. package/src/lib/pricing/seed-snapshot.json +1 -1
  107. package/src/lib/rollout.js +853 -14
  108. package/src/lib/source-metadata.js +1 -1
  109. package/src/lib/trae-cn-config.js +539 -0
  110. package/dashboard/dist/assets/DialogDescription-M_lg2jhD.js +0 -1
  111. package/dashboard/dist/assets/LoginCard-j5HVFRdP.js +0 -1
  112. package/dashboard/dist/assets/ProviderIcon-zaz4nwxI.js +0 -1
  113. package/dashboard/dist/assets/TrendMonitor-BthLuYwy.js +0 -19
  114. package/dashboard/dist/assets/agent-logos-DwHwn_cb.js +0 -1
  115. package/dashboard/dist/assets/module-BRJxSJmz.js +0 -3
  116. package/dashboard/dist/assets/provider-display-BNCdZ83n.js +0 -42
  117. package/dashboard/dist/assets/use-reduced-motion-DbebVWBw.js +0 -1
@@ -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 31 tools. Its distinctive desktop experience includes a pixel pet
4
+ tracker for 32 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 (31)
26
+ ## Supported AI coding tools (33)
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
+ - Prime Agent
44
45
  - Craft Agents
45
46
  - Reasonix
46
47
  - Kilo CLI
@@ -55,6 +56,7 @@ timestamps, never prompts, responses, or source code.
55
56
  - AnythingLLM Desktop
56
57
  - Claude Science
57
58
  - DeepSeek Harness
59
+ - TRAE Work CN
58
60
 
59
61
  ## Capabilities
60
62
  - 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-CRvi_9hb.js"></script>
78
+ <script type="module" crossorigin src="/assets/__vite-browser-external-C6_BtvDo.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.90.2",
4
- "description": "Local-first AI coding token usage and cost tracker for 31 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
3
+ "version": "0.91.3",
4
+ "description": "Local-first AI coding token usage and cost tracker for 33 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",
@@ -65,6 +65,7 @@ const {
65
65
  resolveOmpAgentDir,
66
66
  resolvePiAgentDir,
67
67
  piAgentDirCollidesWithOmp,
68
+ resolvePrimeAgentDir,
68
69
  resolveAnythingllmDbPath,
69
70
  resolveReasonixHome,
70
71
  resolveTraeStoragePath,
@@ -102,7 +103,7 @@ const DIVIDER = "----------------------------------------------";
102
103
  const DEFAULT_DASHBOARD_URL = "https://www.tokentracker.cc";
103
104
 
104
105
  // Single source of truth for the welcome screen's provider count + sample list.
105
- // 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.
106
107
  const SUPPORTED_PROVIDERS = [
107
108
  "Claude Code",
108
109
  "Codex CLI",
@@ -121,6 +122,7 @@ const SUPPORTED_PROVIDERS = [
121
122
  "Grok Build",
122
123
  "oh-my-pi",
123
124
  "pi",
125
+ "Prime Agent",
124
126
  "Craft Agents",
125
127
  "Reasonix",
126
128
  "Kilo CLI",
@@ -135,6 +137,7 @@ const SUPPORTED_PROVIDERS = [
135
137
  "AnythingLLM Desktop",
136
138
  "Claude Science",
137
139
  "DeepSeek Harness",
140
+ "TRAE Work CN",
138
141
  ];
139
142
 
140
143
  async function cmdInit(argv) {
@@ -710,6 +713,14 @@ async function applyIntegrationSetup({
710
713
  }
711
714
  }
712
715
 
716
+ // Prime Agent: metadata-only passive reader — no hook installation needed.
717
+ {
718
+ const primeAgentDir = resolvePrimeAgentDir(process.env);
719
+ if (primeAgentDir && fssync.existsSync(path.join(primeAgentDir, "sessions"))) {
720
+ summary.push({ label: "Prime Agent", status: "detected", detail: "Passive usage reader (no hook needed)" });
721
+ }
722
+ }
723
+
713
724
  // Craft Agents: passive reader — no hook installation needed.
714
725
  // TokenTracker reads ~/.craft-agent/workspaces/<id>/sessions/**/session.jsonl
715
726
  // (and any user-relocated workspace listed in ~/.craft-agent/config.json).
@@ -60,6 +60,8 @@ const {
60
60
  resolvePiSessionFiles,
61
61
  resolvePiAgentDir,
62
62
  piAgentDirCollidesWithOmp,
63
+ resolvePrimeAgentSessionFiles,
64
+ resolvePrimeAgentDir,
63
65
  resolveCraftSessionFiles,
64
66
  resolveCraftConfigDir,
65
67
  resolveReasonixHome,
@@ -87,6 +89,13 @@ const {
87
89
  resolveCopilotAppDbPaths,
88
90
  probeWslDistros,
89
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");
90
99
  const wsl = require("../lib/wsl-probe");
91
100
  const { getWslMode, isInvalidWslMode, shouldProbeWsl, discoverWslHome } = wsl;
92
101
  const { resolveInstallPaths, resolveZcodeNativeDbPath } = require("../lib/install-resolver");
@@ -427,6 +436,11 @@ async function cmdStatus(argv = []) {
427
436
  const piInstalled = !piCollides && Boolean(piAgentDir) && fssync.existsSync(path.join(piAgentDir, "sessions"));
428
437
  const piFiles = piInstalled ? resolvePiSessionFiles(process.env) : [];
429
438
 
439
+ // Prime Agent — passive scan only (no hooks).
440
+ const primeAgentDir = resolvePrimeAgentDir(process.env);
441
+ const primeAgentInstalled = Boolean(primeAgentDir) && fssync.existsSync(path.join(primeAgentDir, "sessions"));
442
+ const primeAgentFiles = primeAgentInstalled ? resolvePrimeAgentSessionFiles(process.env) : [];
443
+
430
444
  // Craft Agents — passive scan only (no hooks).
431
445
  const craftConfigDir = resolveCraftConfigDir(process.env);
432
446
  const craftInstalled = Boolean(craftConfigDir && fssync.existsSync(craftConfigDir));
@@ -635,6 +649,34 @@ async function cmdStatus(argv = []) {
635
649
  ? readTraeEntitlementFromStorage(traeStoragePath)
636
650
  : null;
637
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
+
638
680
  // Grok Build (xAI TUI)
639
681
  const grokHookState = await probeGrokHookState({ home, trackerDir, env: process.env });
640
682
  const grokSessions = grokHookState.hasGrokInstall || grokHookState.sessionsDir
@@ -873,6 +915,9 @@ async function cmdStatus(argv = []) {
873
915
  pi: piInstalled
874
916
  ? { installed: true, files: piFiles.length }
875
917
  : { installed: false },
918
+ prime_agent: primeAgentInstalled
919
+ ? { installed: true, files: primeAgentFiles.length }
920
+ : { installed: false },
876
921
  craft: craftInstalled
877
922
  ? { installed: true, files: craftFiles.length }
878
923
  : { installed: false },
@@ -923,6 +968,14 @@ async function cmdStatus(argv = []) {
923
968
  ...(traeEntitlement ? { entitlement: traeEntitlement } : {}),
924
969
  }
925
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 },
926
979
  grok_build: grokInstalled
927
980
  ? {
928
981
  installed: true,
@@ -1014,6 +1067,9 @@ async function cmdStatus(argv = []) {
1014
1067
  piInstalled
1015
1068
  ? `- pi: passive reader (${piFiles.length} session jsonl file${piFiles.length !== 1 ? "s" : ""} found)`
1016
1069
  : null,
1070
+ primeAgentInstalled
1071
+ ? `- Prime Agent: passive reader (${primeAgentFiles.length} session jsonl file${primeAgentFiles.length !== 1 ? "s" : ""} found)`
1072
+ : null,
1017
1073
  craftInstalled
1018
1074
  ? `- Craft Agents: passive reader (${craftFiles.length} session jsonl file${craftFiles.length !== 1 ? "s" : ""} found)`
1019
1075
  : null,
@@ -1092,6 +1148,9 @@ async function cmdStatus(argv = []) {
1092
1148
  traeEntitlement
1093
1149
  ? `- Trae SOLO plan: ${formatTraeEntitlementLine(traeEntitlement)}`
1094
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,
1095
1154
  ...(() => {
1096
1155
  if (!hermesInstalled) return [];
1097
1156
  return [`- Hermes Agent: state.db found (${hermesPath})`];
@@ -78,6 +78,8 @@ const {
78
78
  resolvePiSessionFiles,
79
79
  parsePiIncremental,
80
80
  piAgentDirCollidesWithOmp,
81
+ resolvePrimeAgentSessionFiles,
82
+ parsePrimeAgentIncremental,
81
83
  resolveCraftSessionFiles,
82
84
  parseCraftIncremental,
83
85
  resolveReasonixTelemetryFiles,
@@ -110,6 +112,7 @@ const {
110
112
  resolveDroidModel,
111
113
  resolveDshSessionFiles,
112
114
  parseDshIncremental,
115
+ parseTraeCnApiIncremental,
113
116
  bucketKey,
114
117
  toUtcHalfHourStart,
115
118
  totalsKey,
@@ -132,6 +135,11 @@ const {
132
135
  parseCursorCsv,
133
136
  } = require("../lib/cursor-config");
134
137
  const { purgeProjectUsage } = require("../lib/project-usage-purge");
138
+ const {
139
+ fetchTraeCnUsageWithAuth,
140
+ resolveTraeCnStoragePath,
141
+ isTraeCnUsageEnabled,
142
+ } = require("../lib/trae-cn-config");
135
143
  const {
136
144
  isCodexSessionCursorPath,
137
145
  isCursorStoreRetry,
@@ -289,6 +297,7 @@ const AUTO_SYNC_SOURCES = new Set([
289
297
  "qoder-cn",
290
298
  "reasonix",
291
299
  "roocode",
300
+ "trae-cn",
292
301
  "workbuddy",
293
302
  "zcode",
294
303
  "zed",
@@ -450,6 +459,15 @@ async function cmdSync(argv, context = {}) {
450
459
  const lockWaitOptions = context && typeof context === "object"
451
460
  ? context.lockWaitOptions
452
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;
453
471
  const syncDiagnostics = diagnostics && typeof diagnostics === "object" ? diagnostics : null;
454
472
  const home = os.homedir();
455
473
  const { trackerDir } = await resolveTrackerPaths({ home });
@@ -1661,6 +1679,93 @@ async function cmdSync(argv, context = {}) {
1661
1679
  }
1662
1680
  }
1663
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
+
1664
1769
  // ── Kiro (SQLite-based, with JSONL fallback; dual-install aware) ──
1665
1770
  let kiroResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1666
1771
  if (sourceAllowed("kiro")) {
@@ -2079,6 +2184,34 @@ async function cmdSync(argv, context = {}) {
2079
2184
  }
2080
2185
  }
2081
2186
 
2187
+ // ── Prime Agent — passive ~/.prime/agent/sessions/*.jsonl usage reader ──
2188
+ let primeAgentResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2189
+ const primeAgentFiles = sourceAllowed("prime-agent")
2190
+ ? mergeBothFileSources({ resolveFiles: resolvePrimeAgentSessionFiles, env: process.env })
2191
+ : [];
2192
+ if (primeAgentFiles.length > 0) {
2193
+ if (progress?.enabled) {
2194
+ progress.start(`Parsing Prime Agent ${renderBar(0)} | buckets 0`);
2195
+ }
2196
+ try {
2197
+ primeAgentResult = await parsePrimeAgentIncremental({
2198
+ sessionFiles: primeAgentFiles,
2199
+ cursors,
2200
+ queuePath,
2201
+ env: process.env,
2202
+ onProgress: (p) => {
2203
+ if (!progress?.enabled) return;
2204
+ const pct = p.total > 0 ? p.index / p.total : 1;
2205
+ progress.update(
2206
+ `Parsing Prime Agent ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
2207
+ );
2208
+ },
2209
+ });
2210
+ } catch (err) {
2211
+ warnProviderParseFailure("Prime Agent", err, opts);
2212
+ }
2213
+ }
2214
+
2082
2215
  // ── Craft Agents (passive ~/.craft-agent + workspaces session.jsonl reader) ──
2083
2216
  let craftResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2084
2217
  const craftFiles = sourceAllowed("craft")
@@ -2127,7 +2260,12 @@ async function cmdSync(argv, context = {}) {
2127
2260
  }
2128
2261
 
2129
2262
  // ── Grok Build (xAI) ──
2130
- let grokResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2263
+ let grokResult = {
2264
+ recordsProcessed: 0,
2265
+ eventsAggregated: 0,
2266
+ bucketsQueued: 0,
2267
+ projectBucketsQueued: 0,
2268
+ };
2131
2269
  // Full passive scan of all Grok sessions (historical + any not covered by hook)
2132
2270
  const grokSessions = sourceAllowed("grok") ? resolveGrokBuildSessions(process.env) : [];
2133
2271
  const grokSessionInputs = [...grokSessions];
@@ -2149,6 +2287,11 @@ async function cmdSync(argv, context = {}) {
2149
2287
  sessionId: hookSessionId,
2150
2288
  sessionDir:
2151
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,
2152
2295
  updatesPath:
2153
2296
  typeof grokHookSignal.updatesPath === "string" ? grokHookSignal.updatesPath : undefined,
2154
2297
  signalsPath:
@@ -2178,6 +2321,7 @@ async function cmdSync(argv, context = {}) {
2178
2321
  sessions: grokSessionInputs,
2179
2322
  cursors,
2180
2323
  queuePath,
2324
+ projectQueuePath,
2181
2325
  env: process.env,
2182
2326
  onProgress: (p) => {
2183
2327
  if (!progress?.enabled) return;
@@ -2194,6 +2338,8 @@ async function cmdSync(argv, context = {}) {
2194
2338
  recordsProcessed: grokResult.recordsProcessed + grokScanResult.recordsProcessed,
2195
2339
  eventsAggregated: grokResult.eventsAggregated + grokScanResult.eventsAggregated,
2196
2340
  bucketsQueued: grokResult.bucketsQueued + grokScanResult.bucketsQueued,
2341
+ projectBucketsQueued:
2342
+ (grokResult.projectBucketsQueued || 0) + (grokScanResult.projectBucketsQueued || 0),
2197
2343
  };
2198
2344
  }
2199
2345
  if (isFullSourceScan && opts.repairGrok) {
@@ -2554,6 +2700,7 @@ async function cmdSync(argv, context = {}) {
2554
2700
  qoderCnResult.recordsProcessed +
2555
2701
  claudeScienceResult.recordsProcessed +
2556
2702
  cursorResult.recordsProcessed +
2703
+ traeCnResult.recordsProcessed +
2557
2704
  kiroResult.recordsProcessed +
2558
2705
  kiroCliResult.recordsProcessed +
2559
2706
  hermesResult.recordsProcessed +
@@ -2563,6 +2710,7 @@ async function cmdSync(argv, context = {}) {
2563
2710
  workbuddyResult.recordsProcessed +
2564
2711
  ompResult.recordsProcessed +
2565
2712
  piResult.recordsProcessed +
2713
+ primeAgentResult.recordsProcessed +
2566
2714
  craftResult.recordsProcessed +
2567
2715
  reasonixResult.recordsProcessed +
2568
2716
  grokResult.recordsProcessed +
@@ -2588,6 +2736,7 @@ async function cmdSync(argv, context = {}) {
2588
2736
  qoderCnResult.bucketsQueued +
2589
2737
  claudeScienceResult.bucketsQueued +
2590
2738
  cursorResult.bucketsQueued +
2739
+ traeCnResult.bucketsQueued +
2591
2740
  kiroResult.bucketsQueued +
2592
2741
  kiroCliResult.bucketsQueued +
2593
2742
  hermesResult.bucketsQueued +
@@ -2597,6 +2746,7 @@ async function cmdSync(argv, context = {}) {
2597
2746
  workbuddyResult.bucketsQueued +
2598
2747
  ompResult.bucketsQueued +
2599
2748
  piResult.bucketsQueued +
2749
+ primeAgentResult.bucketsQueued +
2600
2750
  craftResult.bucketsQueued +
2601
2751
  reasonixResult.bucketsQueued +
2602
2752
  grokResult.bucketsQueued +
@@ -2618,6 +2768,7 @@ async function cmdSync(argv, context = {}) {
2618
2768
  cursorStore.requiresCommit !== true &&
2619
2769
  totalParsed === 0 &&
2620
2770
  totalBuckets === 0 &&
2771
+ !(grokResult.projectBucketsQueued > 0) &&
2621
2772
  !codexColdAuditDue &&
2622
2773
  !codexFallbackRetryRan &&
2623
2774
  !grokHookSignalConsumed &&
@@ -2940,6 +3091,8 @@ function recordCodexColdScanAudit(
2940
3091
  module.exports = {
2941
3092
  cmdSync,
2942
3093
  acquireSyncLock,
3094
+ readQueueBatch,
3095
+ drainQueueToCloud,
2943
3096
  migrateCursorUnknownBuckets,
2944
3097
  migrateRolloutCumulativeDeltaBuckets,
2945
3098
  repairCodebuddyLogJsonlOverlap,
@@ -3440,7 +3593,10 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
3440
3593
  for (let batch = 0; batch < maxBatches; batch++) {
3441
3594
  if (offset >= queueSize) break;
3442
3595
  const result = await readQueueBatch(queuePath, offset, limit);
3443
- 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;
3444
3600
 
3445
3601
  const root = baseUrl.replace(/\/$/, "");
3446
3602
  const anonKey = process.env.TOKENTRACKER_INSFORGE_ANON_KEY || "";
@@ -3453,7 +3609,12 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
3453
3609
  const res = await fetch(`${root}/functions/${INGEST_SLUG}`, {
3454
3610
  method: "POST",
3455
3611
  headers,
3456
- 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
+ }),
3457
3618
  });
3458
3619
 
3459
3620
  const rawText = await res.text().catch(() => "");
@@ -3490,6 +3651,26 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
3490
3651
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
3491
3652
 
3492
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();
3493
3674
  let offset = startOffset;
3494
3675
  let linesRead = 0;
3495
3676
  for await (const line of rl) {
@@ -3502,6 +3683,37 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
3502
3683
  } catch (_e) {
3503
3684
  continue;
3504
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;
3505
3717
  const hourStart = typeof bucket?.hour_start === "string" ? bucket.hour_start : null;
3506
3718
  if (!hourStart) continue;
3507
3719
  const source = (typeof bucket?.source === "string" ? bucket.source.trim().toLowerCase() : "") || "codex";
@@ -3521,7 +3733,11 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
3521
3733
 
3522
3734
  rl.close();
3523
3735
  stream.close?.();
3524
- 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
+ };
3525
3741
  }
3526
3742
 
3527
3743
  function normalizeGrokRepairSource(value) {