tokentracker-cli 1.1.3 → 1.1.5

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 (120) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +20 -4
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{AchievementBadge-dt_UDU_h.js → AchievementBadge-ClUmGxBJ.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-B5DJ3S9l.js → AchievementsPage-BB0DFE3m.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-CDxcyig2.js → AchievementsSection-BMCSk63s.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-Cp98zNn2.js → BadgeDetailModal-DzJtvuKO.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-CR7MiUpo.js → BotAnimated-BviEasW4.js} +1 -1
  11. package/dashboard/dist/assets/{Card-C8IdDvlg.js → Card-BoHr-wLc.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-COeTlW8z.js → ClawdAnimated-Bwm8BfqY.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-Co9k2CCc.js → CommandPalette-Rb0Gfcg9.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-C6XQeRgw.js → CommunityStatsModal-Ck7R7Q-d.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-Ai4182O-.js → ConfirmModal-52Fu9wOi.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-BbKzN-Pe.js → Controls-YlGr187A.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-BRim2Rt6.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-CZbrtJmb.js → DevicePage-3mmhzfgd.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-DMl8tAoX.js → DialogClose-D1KfBYUS.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-BhWwQSSg.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-BJxpp2t-.js → DialogTitle-DKye2nJK.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-hU_MXl6R.js → FadeIn-mJp_ZFst.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-CM5RSxxN.js → HeaderGithubStar-B2R_aqp9.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-BZKfQyZQ.js → HoverTooltip-ElY02914.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-BjDx-zja.js → IpCheckPage-CNzPUe3x.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-CAt8BCDv.js → LandingPage-L25-CxXg.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-DiCrakjI.js → LeaderboardAvatar-BT7x5wAP.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-Dj3pHv6U.js → LeaderboardPage-bPce9gOU.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-DEWbcP9p.js → LeaderboardProfileModal-DPrFGAp6.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-CTbWHQkY.js → LeaderboardProfilePage-j2D1pwYO.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-V-98OUhF.js → LimitsPage-CSED1kBx.js} +1 -1
  32. package/dashboard/dist/assets/{LocalOnlyNotice-a4PqtUZY.js → LocalOnlyNotice-D6OERWez.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-oD1A2Ru8.js → LoginCard-HgmRdIn8.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-D8aSmxok.js → LoginModal-BSlEmKOV.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-DIjhBv2X.js → LoginPage-HQAhowvh.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-D7601BTX.js → PetPage-DnbYfu-z.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-aPnmKopO.js → PopoverPopup-DnyVdfB8.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-rwyfcNlT.js → ProviderIcon-D8kgkZlR.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-AF3SBgW2.js → ResetPasswordPage-ChtXGykP.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-fnlScNMT.js → SegmentedControl-dHUpXqFk.js} +1 -1
  41. package/dashboard/dist/assets/{Select-DDjC3RjB.js → Select-C0tpjNiq.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-VhBWE2O8.js → SelectItemText-C3G_w14k.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-BL3qqEsp.js → ServiceStatusPage-D6x4e1r8.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-DXYiuP2x.js → SessionsPage-DZH1QMWd.js} +1 -1
  45. package/dashboard/dist/assets/{SettingsPage-DTeA_ahr.js → SettingsPage-DGmDb2R4.js} +2 -2
  46. package/dashboard/dist/assets/{SkillsPage-CtTRwVQH.js → SkillsPage-ChloCHPa.js} +1 -1
  47. package/dashboard/dist/assets/{TokenGalaxy-CifK91V7.js → TokenGalaxy-DOC9wlkv.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-ClGAIYoa.js → ToolbarRootContext-BB_d-qdY.js} +1 -1
  49. package/dashboard/dist/assets/{TrendMonitor-D1evFHRM.js → TrendMonitor-DVVzYdc7.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-Cq4RYJDE.js → WidgetsPage-DHVSyU0i.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-CoC5RJH7.js → WrappedPage-BDi28nRt.js} +1 -1
  52. package/dashboard/dist/assets/__vite-browser-external-Bnc5aBhq.js +45 -0
  53. package/dashboard/dist/assets/{agent-logos-QLdA9OeA.js → agent-logos-BiGvZiLq.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-BYLgB3bH.js → arrow-up-right-SvD82rok.js} +1 -1
  55. package/dashboard/dist/assets/{check-DfnIvqZD.js → check-jL5XErEW.js} +1 -1
  56. package/dashboard/dist/assets/{composite--JllTpIX.js → composite-DM6geXPA.js} +1 -1
  57. package/dashboard/dist/assets/{copy-ClKVBIgD.js → copy-Dy_GIzj2.js} +1 -1
  58. package/dashboard/dist/assets/{download-Di1-UFJX.js → download-BW_FS7nX.js} +1 -1
  59. package/dashboard/dist/assets/{external-link-D7zPVFLO.js → external-link-2RIovvCT.js} +1 -1
  60. package/dashboard/dist/assets/{flame-BMJTxjVg.js → flame-CIRJIxxA.js} +1 -1
  61. package/dashboard/dist/assets/{format-tokens-DCZM5Nfn.js → format-tokens-CT8ahEK4.js} +1 -1
  62. package/dashboard/dist/assets/{icons-I615W2IU.js → icons-7J1WD0C-.js} +1 -1
  63. package/dashboard/dist/assets/{index-BX3CXVdD.js → index-D3iSSyYr.js} +1 -1
  64. package/dashboard/dist/assets/{index-CwczM3BZ.js → index-Ddb2A6Mx.js} +1 -1
  65. package/dashboard/dist/assets/{index-BejnmdPe.js → index-_vJl6sF7.js} +1 -1
  66. package/dashboard/dist/assets/{info-CFJPny5Q.js → info-BjVWcOOC.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-D1ydWh_w.js → link-2-CyRWnHWE.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-Cq-J-OFg.js → loader-circle-DUpG5zah.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-kIoSq57I.js → maximize-2-BVGCRkFZ.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-CLojthuF.js → modifiers.esm-CZI7rED4.js} +1 -1
  71. package/dashboard/dist/assets/module-WSx8viXK.js +3 -0
  72. package/dashboard/dist/assets/{monitor-smartphone-DX2qWHYV.js → monitor-smartphone-DfBtd01T.js} +1 -1
  73. package/dashboard/dist/assets/{plus-B-JEpo3V.js → plus-CvbLCvjV.js} +1 -1
  74. package/dashboard/dist/assets/{provider-display-DJDdrTAS.js → provider-display-DC46GLrv.js} +2 -2
  75. package/dashboard/dist/assets/{react-Ctoe8iLl.js → react-mYr3J8Vw.js} +1 -1
  76. package/dashboard/dist/assets/{search-BTSQrlW-.js → search-CFaS1rSj.js} +1 -1
  77. package/dashboard/dist/assets/{skills-api-DKQyM8h7.js → skills-api-B1N6seFa.js} +1 -1
  78. package/dashboard/dist/assets/{sortable.esm-pOEL13xU.js → sortable.esm-BE2GiQaC.js} +1 -1
  79. package/dashboard/dist/assets/{store-B2RplzvW.js → store-DwBbExah.js} +1 -1
  80. package/dashboard/dist/assets/{terminal-BH_6_EW_.js → terminal-Bl2BuO9x.js} +1 -1
  81. package/dashboard/dist/assets/{trash-CC444Mz6.js → trash-D8beb27Y.js} +1 -1
  82. package/dashboard/dist/assets/{use-community-stats-CBXLdD_j.js → use-community-stats-DJG1DsXR.js} +1 -1
  83. package/dashboard/dist/assets/{use-latest-request-guard-DMuPfZQI.js → use-latest-request-guard-DYK8iyOM.js} +1 -1
  84. package/dashboard/dist/assets/{use-limits-display-prefs-CmsGTBA4.js → use-limits-display-prefs-BGmcFVX3.js} +1 -1
  85. package/dashboard/dist/assets/{use-ordered-list-BNJNpXjB.js → use-ordered-list-Cqia_VIq.js} +1 -1
  86. package/dashboard/dist/assets/use-reduced-motion-CmCk9Sb5.js +1 -0
  87. package/dashboard/dist/assets/{use-session-efficiency-pref-g_0ISxe1.js → use-session-efficiency-pref-Mj3OeVGZ.js} +1 -1
  88. package/dashboard/dist/assets/{use-transform-D36BHcXk.js → use-transform-D4BQIBHx.js} +1 -1
  89. package/dashboard/dist/assets/{use-usage-limits-Dxw3vzsD.js → use-usage-limits-BFrO7Mtq.js} +1 -1
  90. package/dashboard/dist/assets/{useCurrency-V2FE4TMo.js → useCurrency-CYJavR2h.js} +1 -1
  91. package/dashboard/dist/assets/{useTokenFormat-CEOwvzMt.js → useTokenFormat-CKruI7SP.js} +1 -1
  92. package/dashboard/dist/assets/{zap-BS7ZU76I.js → zap-a82fG76c.js} +1 -1
  93. package/dashboard/dist/brand-logos/trae.svg +1 -0
  94. package/dashboard/dist/index.html +13 -11
  95. package/dashboard/dist/ip-check.html +1 -1
  96. package/dashboard/dist/leaderboard.html +1 -1
  97. package/dashboard/dist/llms.txt +3 -2
  98. package/dashboard/dist/share.html +1 -1
  99. package/package.json +6 -4
  100. package/src/commands/init.js +9 -7
  101. package/src/commands/status.js +66 -11
  102. package/src/commands/sync.js +115 -11
  103. package/src/lib/codex-context-breakdown.js +3 -1
  104. package/src/lib/diagnostics.js +20 -1
  105. package/src/lib/doctor.js +30 -0
  106. package/src/lib/pricing/curated-overrides.json +7 -0
  107. package/src/lib/pricing/matcher.js +5 -0
  108. package/src/lib/pricing/seed-snapshot.json +1 -1
  109. package/src/lib/rollout.js +197 -18
  110. package/src/lib/scan-roots.js +277 -0
  111. package/src/lib/session-analytics.js +98 -16
  112. package/src/lib/trae-db.js +127 -0
  113. package/src/lib/trae-snapshot.js +230 -0
  114. package/src/lib/trae-sqlite.js +143 -0
  115. package/src/lib/trae-usage.js +121 -0
  116. package/dashboard/dist/assets/DashboardPage-DTcVGG4e.js +0 -1
  117. package/dashboard/dist/assets/DialogDescription-haPz7CX9.js +0 -1
  118. package/dashboard/dist/assets/__vite-browser-external-BDugA6I0.js +0 -45
  119. package/dashboard/dist/assets/module-DtWfRKM4.js +0 -3
  120. package/dashboard/dist/assets/use-reduced-motion-CNpG-Yoi.js +0 -1
@@ -116,6 +116,13 @@ const {
116
116
  const wsl = require("../lib/wsl-probe");
117
117
  const { getWslMode, isInvalidWslMode, shouldProbeWsl, discoverWslHome } = wsl;
118
118
  const { resolveInstallPaths, resolveZcodeNativeDbPath, resolveMimoNativeDbPath } = require("../lib/install-resolver");
119
+ const {
120
+ describeScanRootOrigin,
121
+ describeScanRootState,
122
+ hasAnyScanChild,
123
+ resolveEnvRoot,
124
+ resolveScanRoots,
125
+ } = require("../lib/scan-roots");
119
126
  const { probeGrokHookState, resolveGrokHome } = require("../lib/grok-hook");
120
127
  const { probeOmpHookState } = require("../lib/omp-hook");
121
128
 
@@ -197,7 +204,9 @@ async function cmdStatus(argv = []) {
197
204
  const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
198
205
  const autoRetryPath = path.join(trackerDir, "auto.retry.json");
199
206
  const syncSkipPath = path.join(trackerDir, "sync.skip.json");
200
- const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
207
+ // Normalized like sync (a relative CODEX_HOME is anchored to home, not cwd)
208
+ // so status lists exactly the root sync walks.
209
+ const codexHome = resolveEnvRoot("codex", { env: process.env, home }) || path.join(home, ".codex");
201
210
  const codexConfigPath = path.join(codexHome, "config.toml");
202
211
  const acodeHome = process.env.TOKENTRACKER_ACODE_HOME || path.join(home, ".acode");
203
212
  const acodeConfigPath = path.join(acodeHome, "config.toml");
@@ -229,6 +238,33 @@ async function cmdStatus(argv = []) {
229
238
  const geminiHookCommand = buildGeminiHookCommand(notifyPath);
230
239
 
231
240
  const config = await readJson(configPath);
241
+ // Extra scan roots (#657): CODEX_HOME / CLAUDE_CONFIG_DIR of this process
242
+ // plus config.scanRoots, resolved exactly as sync does, so status lists every
243
+ // root sync walks — this is the output users paste when usage looks wrong.
244
+ const scanRoots = resolveScanRoots({
245
+ home,
246
+ env: process.env,
247
+ config,
248
+ base: { codex: [codexHome], claude: [path.join(home, ".claude")] },
249
+ });
250
+ const extraScanRoots = [];
251
+ for (const provider of ["codex", "claude"]) {
252
+ for (const entry of scanRoots[provider]) {
253
+ if (entry.origin === "native") continue;
254
+ extraScanRoots.push({
255
+ provider,
256
+ origin: describeScanRootOrigin(entry, provider),
257
+ path: entry.path,
258
+ exists: entry.exists,
259
+ error: entry.error || null,
260
+ });
261
+ }
262
+ }
263
+ const describeExtraScanRoot = (root) =>
264
+ `${root.provider} ${root.origin}: ${root.path}${describeScanRootState(root)}`;
265
+ const scanRootsLine = extraScanRoots.length > 0
266
+ ? `- Extra scan roots: ${extraScanRoots.map(describeExtraScanRoot).join(" | ")}`
267
+ : null;
232
268
  const { cursors } = await readCursorStateSummary({ trackerDir, cursorsPath });
233
269
  const codexRecordOnlyWarning = formatRecordOnlyWarning(countRecordOnlyFiles(cursors));
234
270
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
@@ -419,6 +455,10 @@ async function cmdStatus(argv = []) {
419
455
  ? wsl.discoverWslHome(".claude")
420
456
  : null;
421
457
  if (wslClaudeHomeStatus) claudeHomesStatus.push({ dir: wslClaudeHomeStatus, label: "WSL" });
458
+ for (const entry of scanRoots.claude) {
459
+ if (entry.origin === "native" || !entry.exists) continue;
460
+ claudeHomesStatus.push({ dir: entry.path, label: describeScanRootOrigin(entry, "claude") });
461
+ }
422
462
  for (const { dir, label } of claudeHomesStatus) {
423
463
  const projects = path.join(dir, "projects");
424
464
  try {
@@ -657,7 +697,7 @@ async function cmdStatus(argv = []) {
657
697
  // users are asked to paste when Codex usage looks wrong, so it must list
658
698
  // every root sync actually walks — and no empty shell sync would skip.
659
699
  const codexPaths = resolveInstallPaths({
660
- nativeValue: process.env.CODEX_HOME || path.join(home, ".codex"),
700
+ nativeValue: codexHome,
661
701
  wslDir: ".codex",
662
702
  requireAnyChild: ["sessions", "archived_sessions"],
663
703
  union: true,
@@ -665,6 +705,11 @@ async function cmdStatus(argv = []) {
665
705
  // Both children, matching requireAnyChild above: an install holding only
666
706
  // archived_sessions/ is counted by sync and must not read as "not detected".
667
707
  const codexActive = formatResolvedPaths(codexPaths, ["sessions", "archived_sessions"]);
708
+ for (const entry of scanRoots.codex) {
709
+ if (entry.origin === "native" || !entry.exists) continue;
710
+ if (!hasAnyScanChild(entry.path, ["sessions", "archived_sessions"])) continue;
711
+ codexActive.push(`${describeScanRootOrigin(entry, "codex")}: ${entry.path}`);
712
+ }
668
713
  const codexInstalledStatus = codexActive.length > 0;
669
714
 
670
715
  const acodePaths = resolveInstallPaths({
@@ -755,9 +800,10 @@ async function cmdStatus(argv = []) {
755
800
  const devinDbPath = resolveDevinDbPath(process.env);
756
801
  const devinInstalled = Boolean(devinDbPath && fssync.existsSync(devinDbPath));
757
802
 
758
- // Trae SOLO (ByteDance AI IDE) — passive entitlement snapshot reader.
803
+ const { resolveTraeDbPaths } = require("../lib/trae-db");
804
+ const traeDbPaths = resolveTraeDbPaths(process.env);
759
805
  const traeStoragePath = resolveTraeStoragePath(process.env);
760
- const traeInstalled = Boolean(traeStoragePath);
806
+ const traeInstalled = Boolean(traeStoragePath || traeDbPaths.length);
761
807
  // Render path for the entitlement snapshot: read it straight from the
762
808
  // Trae Local State storage.json via the shared parser. The queue stays
763
809
  // token-count-only, so the status read path never depends on queue rows
@@ -982,6 +1028,10 @@ async function cmdStatus(argv = []) {
982
1028
  last_upload_error: lastUploadError || null,
983
1029
  last_sync_skipped: syncSkip?.at ? syncSkip : null,
984
1030
  auto_retry: autoRetry || null,
1031
+ // Extra scan roots (#657): CODEX_HOME / CLAUDE_CONFIG_DIR of this process
1032
+ // plus config.scanRoots; `exists` false + `error` null means absent,
1033
+ // `error` set means present but unreadable.
1034
+ extra_scan_roots: extraScanRoots,
985
1035
  hooks: {
986
1036
  codex_notify: notifyConfigured,
987
1037
  acode_notify: acodeConfigured,
@@ -1124,7 +1174,9 @@ async function cmdStatus(argv = []) {
1124
1174
  trae: traeInstalled
1125
1175
  ? {
1126
1176
  installed: true,
1127
- detail: traeStoragePath,
1177
+ detail: traeDbPaths[0] || traeStoragePath,
1178
+ usage_databases: traeDbPaths.length,
1179
+ usage_key_source: process.env.TOKENTRACKER_TRAE_SQLCIPHER_KEY?.trim() ? "environment" : "application",
1128
1180
  ...(traeEntitlement ? { entitlement: traeEntitlement } : {}),
1129
1181
  }
1130
1182
  : { installed: false },
@@ -1195,6 +1247,7 @@ async function cmdStatus(argv = []) {
1195
1247
  lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
1196
1248
  syncSkipLine,
1197
1249
  autoRetryLine,
1250
+ scanRootsLine,
1198
1251
  `- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : "unset"}`,
1199
1252
  `- AStudio notify: ${acodeConfigured ? JSON.stringify(acodeNotify) : "unset"}`,
1200
1253
  `- Every Code notify: ${everyCodeConfigured ? JSON.stringify(everyCodeNotify) : "unset"}`,
@@ -1328,12 +1381,9 @@ async function cmdStatus(argv = []) {
1328
1381
  ? `- Devin CLI: passive reader (${devinDbPath})`
1329
1382
  : null,
1330
1383
  traeInstalled
1331
- // Deliberately NOT "passive reader": every other line with that wording
1332
- // means tokens are being counted. Trae encrypts its session transcripts
1333
- // (SQLCipher) and its plaintext summaries carry no token counts, so this
1334
- // provider contributes plan info and nothing else — say so, or users go
1335
- // looking for Trae usage in the dashboard that will never appear.
1336
- ? `- Trae SOLO: plan info only, no token usage (${traeStoragePath})`
1384
+ ? traeDbPaths.length
1385
+ ? `- TRAE: local usage reader, ${traeDbPaths.length} database(s) (shared application key; optional key override)`
1386
+ : `- TRAE: plan info only, no local usage database found (${traeStoragePath})`
1337
1387
  : null,
1338
1388
  traeEntitlement
1339
1389
  ? `- Trae SOLO plan: ${formatTraeEntitlementLine(traeEntitlement)}`
@@ -1525,6 +1575,11 @@ function renderLightTable(summary) {
1525
1575
  push(`Hook · ${name}`, state ? "set" : "unset");
1526
1576
  }
1527
1577
 
1578
+ for (const root of summary.extra_scan_roots || []) {
1579
+ const state = root.exists ? "" : (root.error ? ` (unreadable: ${root.error})` : " (missing)");
1580
+ push(`Scan root · ${root.provider}`, `${root.origin}: ${root.path}${state}`);
1581
+ }
1582
+
1528
1583
  for (const [name, info] of Object.entries(summary.providers || {})) {
1529
1584
  const detail = [];
1530
1585
  if (typeof info.installed === "boolean") detail.push(info.installed ? "installed" : "not installed");
@@ -138,6 +138,7 @@ const {
138
138
  resolveCommandCodeSessionFiles,
139
139
  parseCommandCodeIncremental,
140
140
  parseTraeCnApiIncremental,
141
+ parseTraeIncremental,
141
142
  bucketKey,
142
143
  toUtcHalfHourStart,
143
144
  totalsKey,
@@ -171,6 +172,14 @@ const {
171
172
  openCursorStore,
172
173
  } = require("../lib/cursor-store");
173
174
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
175
+ const {
176
+ appendUniqueDirs,
177
+ extraScanRootPaths,
178
+ hasAnyScanChild,
179
+ resolveEnvRoot,
180
+ resolveScanRoots,
181
+ scanRootDirState,
182
+ } = require("../lib/scan-roots");
174
183
  const { resolveRuntimeConfig, isLegacyInsforgeBaseUrl } = require("../lib/runtime-config");
175
184
  const { extractTokenCount } = require("../lib/codex-rollout-parser");
176
185
  const {
@@ -333,6 +342,7 @@ const AUTO_SYNC_SOURCES = new Set([
333
342
  "reasonix",
334
343
  "roocode",
335
344
  "trae-cn",
345
+ "trae",
336
346
  "unsloth",
337
347
  "workbuddy",
338
348
  "zcode",
@@ -599,7 +609,24 @@ async function cmdSync(argv, context = {}) {
599
609
  persistedAnonKey: config.anonKey,
600
610
  };
601
611
  }
602
- const codexCursorRoots = [process.env.CODEX_HOME || path.join(home, ".codex")];
612
+ // Scan roots (#657). Every producer — hook-fired sync, native background
613
+ // refresh, CLI — must derive the SAME root list, and the cursor store's
614
+ // codexRoots must come from that list: a rollout under a root the store
615
+ // does not know is filed in core.json instead of its per-day shard, so two
616
+ // producers with different roots never see each other's cursor and re-parse
617
+ // the file from byte 0 on every alternation (#639). CODEX_HOME keeps its
618
+ // existing meaning (replaces ~/.codex for this process) but is normalized
619
+ // once by resolveEnvRoot — a relative value is anchored to home, not cwd —
620
+ // and that single value feeds discovery AND the cursor store below.
621
+ // config.scanRoots adds roots for every process.
622
+ const codexNativeValue = resolveEnvRoot("codex", { env: process.env, home }) || path.join(home, ".codex");
623
+ const scanRoots = resolveScanRoots({
624
+ home,
625
+ env: process.env,
626
+ config,
627
+ base: { codex: [codexNativeValue], claude: [path.join(home, ".claude")] },
628
+ });
629
+ const codexCursorRoots = scanRoots.codex.map((entry) => entry.path);
603
630
  const cursorStore = await openCursorStore({
604
631
  trackerDir,
605
632
  cursorsPath,
@@ -641,7 +668,16 @@ async function cmdSync(argv, context = {}) {
641
668
  claudeInstallHomes.push(claudeNativeHome);
642
669
  }
643
670
  if (wslClaudeHome) claudeInstallHomes.push(wslClaudeHome);
644
- const claudeProjectsDirs = claudeInstallHomes.map((h) => path.join(h, "projects"));
671
+ // Extra Claude roots (#657): CLAUDE_CONFIG_DIR of the spawning process and
672
+ // config.scanRoots.claude, additive to the homes above so coverage does not
673
+ // depend on which process spawned this sync. Deduped by realpath at the
674
+ // projects/ level: two profiles may symlink one projects/ dir, and reading
675
+ // it under both spellings would double-parse every file, leaving
676
+ // correctness to the bounded claudeHashes layer.
677
+ const claudeProjectsDirs = appendUniqueDirs(
678
+ claudeInstallHomes.map((h) => path.join(h, "projects")),
679
+ extraScanRootPaths(scanRoots.claude).map((h) => path.join(h, "projects")),
680
+ );
645
681
  const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
646
682
  const kiloHome = process.env.KILO_HOME || path.join(xdgDataHome, "kilo");
647
683
 
@@ -698,7 +734,6 @@ async function cmdSync(argv, context = {}) {
698
734
 
699
735
  const sources = [];
700
736
  if (sourceAllowed("codex")) {
701
- const codexNativeValue = process.env.CODEX_HOME || path.join(home, ".codex");
702
737
  // resolveInstallPaths stays the single authority for wsl-first /
703
738
  // native-first / wsl-only / native-only / both selection; requireAnyChild
704
739
  // makes it validate that a candidate actually holds sessions/ or
@@ -738,6 +773,17 @@ async function cmdSync(argv, context = {}) {
738
773
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "archived_sessions"), deep: true });
739
774
  }
740
775
  }
776
+ // Extra Codex roots (#657) from config.scanRoots.codex. Same populated-
777
+ // root rule as requireAnyChild above so an empty shell dir is not walked;
778
+ // the day-inventory cache is keyed by day directory, so sharing it across
779
+ // roots is safe.
780
+ for (const extraRoot of extraScanRootPaths(scanRoots.codex)) {
781
+ if (!hasAnyScanChild(extraRoot, ["sessions", "archived_sessions"])) continue;
782
+ sources.push({ source: "codex", sessionsDir: path.join(extraRoot, "sessions"), inventoryCacheKey: "codexDayInventoryCache" });
783
+ if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
784
+ sources.push({ source: "codex", sessionsDir: path.join(extraRoot, "archived_sessions"), deep: true });
785
+ }
786
+ }
741
787
  }
742
788
  if (sourceAllowed("acode")) {
743
789
  const acodeNativeValue =
@@ -1065,14 +1111,49 @@ async function cmdSync(argv, context = {}) {
1065
1111
  }
1066
1112
  if (isFullSourceScan) {
1067
1113
  await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath, queueStatePath });
1068
- await repairClaudeQueueFromGroundTruth({
1069
- cursors,
1070
- queuePath,
1071
- queueStatePath,
1072
- projectQueuePath,
1073
- projectQueueStatePath,
1074
- rootDirs: claudeProjectsDirs,
1075
- });
1114
+ // The ground-truth repair rebuilds every Claude queue row from the roots
1115
+ // it is given. A configured root that is absent or unreadable right now
1116
+ // (unmounted volume, permissions) may still hold history that an earlier
1117
+ // scoped sync queued, so rebuilding without it would erase that history.
1118
+ // The same applies one level down: listClaudeProjectFiles turns a read
1119
+ // error on projects/ into an empty listing, so an unreadable projects/
1120
+ // would let the repair run against nothing and mark itself complete. An
1121
+ // ABSENT projects/ is fine for a NEW root (a profile with no sessions
1122
+ // yet) but not for one the cursor store shows has supplied files before:
1123
+ // then the directory vanished and rebuilding without it would erase its
1124
+ // history. Defer in both cases: the migration key stays unset and the
1125
+ // repair runs on a later full scan once the root is back. Ordinary
1126
+ // scanning still proceeds.
1127
+ const cursorFilePaths = Object.keys(cursors.files || {});
1128
+ const rootPreviouslySuppliedFiles = (rootPath) => {
1129
+ const prefix = path.join(rootPath, "projects") + path.sep;
1130
+ return cursorFilePaths.some((filePath) => filePath.startsWith(prefix));
1131
+ };
1132
+ const unavailableClaudeRoots = scanRoots.claude
1133
+ .filter((entry) => {
1134
+ if (entry.origin === "native") return false;
1135
+ if (!entry.exists) return true;
1136
+ const projectsState = scanRootDirState(path.join(entry.path, "projects"));
1137
+ if (projectsState.error !== null) return true;
1138
+ return !projectsState.exists && rootPreviouslySuppliedFiles(entry.path);
1139
+ })
1140
+ .map((entry) => entry.path);
1141
+ if (unavailableClaudeRoots.length > 0) {
1142
+ if (!opts.auto) {
1143
+ process.stderr.write(
1144
+ `Claude ground-truth repair deferred: configured scan root(s) unavailable: ${unavailableClaudeRoots.join(", ")}\n`,
1145
+ );
1146
+ }
1147
+ } else {
1148
+ await repairClaudeQueueFromGroundTruth({
1149
+ cursors,
1150
+ queuePath,
1151
+ queueStatePath,
1152
+ projectQueuePath,
1153
+ projectQueueStatePath,
1154
+ rootDirs: claudeProjectsDirs,
1155
+ });
1156
+ }
1076
1157
  }
1077
1158
  let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1078
1159
  if (claudeFiles.length > 0) {
@@ -2011,6 +2092,27 @@ async function cmdSync(argv, context = {}) {
2011
2092
  }
2012
2093
  }
2013
2094
 
2095
+ let traeResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2096
+ if (sourceAllowed("trae")) {
2097
+ try {
2098
+ traeResult = await parseTraeIncremental({
2099
+ cursors, queuePath, onProgress: makeProviderProgress("TRAE"),
2100
+ });
2101
+ for (const { database, message } of traeResult.errors) {
2102
+ process.stderr.write(`TRAE sync: could not read ${database}: ${message}. Will retry on the next sync.\n`);
2103
+ }
2104
+ if (traeResult.recordsSkipped > 0 && !opts.auto) {
2105
+ process.stderr.write(`TRAE sync: skipped ${traeResult.recordsSkipped} records with unsupported usage metadata.\n`);
2106
+ }
2107
+ if (traeResult.estimatedRecords > 0 && !opts.auto) {
2108
+ process.stderr.write(`TRAE sync: ${traeResult.estimatedRecords} Gemini records have repaired thought or cache counters, marked as estimated.\n`);
2109
+ }
2110
+ if (traeResult.unpricedRecords > 0 && !opts.auto) {
2111
+ process.stderr.write(`TRAE sync: ${traeResult.unpricedRecords} multi-request turns include earlier input without a cache split; it is counted in token totals but left out of cost.\n`);
2112
+ }
2113
+ } catch (err) { warnProviderParseFailure("TRAE", err); }
2114
+ }
2115
+
2014
2116
  // ── Trae Work CN (国内版) — account-level usage API ──
2015
2117
  // A simple explicit rolling 30-day window captured once per sync (no
2016
2118
  // cursor checkpoints / full-history import / page retries / background
@@ -3101,6 +3203,7 @@ async function cmdSync(argv, context = {}) {
3101
3203
  claudeScienceResult.recordsProcessed +
3102
3204
  cursorResult.recordsProcessed +
3103
3205
  traeCnResult.recordsProcessed +
3206
+ traeResult.recordsProcessed +
3104
3207
  kiroResult.recordsProcessed +
3105
3208
  kiroCliResult.recordsProcessed +
3106
3209
  hermesResult.recordsProcessed +
@@ -3144,6 +3247,7 @@ async function cmdSync(argv, context = {}) {
3144
3247
  claudeScienceResult.bucketsQueued +
3145
3248
  cursorResult.bucketsQueued +
3146
3249
  traeCnResult.bucketsQueued +
3250
+ traeResult.bucketsQueued +
3147
3251
  kiroResult.bucketsQueued +
3148
3252
  kiroCliResult.bucketsQueued +
3149
3253
  hermesResult.bucketsQueued +
@@ -15,6 +15,8 @@ const fs = require("node:fs");
15
15
  const os = require("node:os");
16
16
  const path = require("node:path");
17
17
 
18
+ const { resolveEnvRoot } = require("./scan-roots");
19
+
18
20
  const {
19
21
  emptyTotals,
20
22
  addInto,
@@ -396,7 +398,7 @@ async function computeCodexContextBreakdown({
396
398
  toKey = range?.to || null;
397
399
  }
398
400
 
399
- const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
401
+ const codexHome = resolveEnvRoot("codex", { env: process.env, home: os.homedir() }) || path.join(os.homedir(), ".codex");
400
402
  const roots = codexDir
401
403
  ? [codexDir]
402
404
  : [path.join(codexHome, "sessions"), path.join(codexHome, "archived_sessions")];
@@ -3,6 +3,7 @@ const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
 
5
5
  const { readJson } = require("./fs");
6
+ const { resolveEnvRoot, resolveScanRoots } = require("./scan-roots");
6
7
  const { readCursorStateSummary } = require("./cursor-store");
7
8
  const { readCodexNotify, readEveryCodeNotify } = require("./codex-config");
8
9
  const { areClaudeUsageHooksConfigured, buildClaudeHookCommand } = require("./claude-config");
@@ -66,7 +67,7 @@ function resolveKiroCliDbPathInline(env, home) {
66
67
 
67
68
  async function collectTrackerDiagnostics({
68
69
  home = os.homedir(),
69
- codexHome = process.env.CODEX_HOME || path.join(home, ".codex"),
70
+ codexHome = resolveEnvRoot("codex", { env: process.env, home }) || path.join(home, ".codex"),
70
71
  codeHome = process.env.CODE_HOME || path.join(home, ".code"),
71
72
  } = {}) {
72
73
  const { trackerDir, binDir } = await resolveTrackerPaths({ home });
@@ -92,6 +93,20 @@ async function collectTrackerDiagnostics({
92
93
  path.join(home, ".grok");
93
94
 
94
95
  const config = await readJson(configPath);
96
+ // Extra scan roots (#657), resolved as sync does. Paths are home-redacted
97
+ // like every other entry in `paths`.
98
+ const scanRoots = resolveScanRoots({
99
+ home,
100
+ env: process.env,
101
+ config,
102
+ base: { codex: [codexHome], claude: [path.join(home, ".claude")] },
103
+ });
104
+ const describeScanRoots = (entries) => entries.map((entry) => ({
105
+ path: redactValue(entry.path, home),
106
+ origin: entry.origin,
107
+ exists: entry.exists,
108
+ error: entry.error || null,
109
+ }));
95
110
  const cursorSummary = await readCursorStateSummary({ trackerDir, cursorsPath });
96
111
  const cursors = cursorSummary.cursors;
97
112
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
@@ -217,6 +232,10 @@ async function collectTrackerDiagnostics({
217
232
  merge_policy:
218
233
  "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.",
219
234
  },
235
+ scan_roots: {
236
+ codex: describeScanRoots(scanRoots.codex),
237
+ claude: describeScanRoots(scanRoots.claude),
238
+ },
220
239
  config: {
221
240
  base_url: typeof config?.baseUrl === "string" ? config.baseUrl : null,
222
241
  device_token: config?.deviceToken ? "set" : "unset",
package/src/lib/doctor.js CHANGED
@@ -337,6 +337,36 @@ function buildDiagnosticsChecks(diagnostics) {
337
337
  });
338
338
  }
339
339
 
340
+ // Extra scan roots (#657): surface CODEX_HOME / CLAUDE_CONFIG_DIR / config
341
+ // roots in the human-readable report and warn when a configured root is not
342
+ // on disk — a silently missing root is the difference between "no usage"
343
+ // and "not scanned". Installs without extras get no additional line.
344
+ const scanRoots = diagnostics?.scan_roots;
345
+ if (scanRoots && typeof scanRoots === "object") {
346
+ const extras = [];
347
+ for (const provider of ["codex", "claude"]) {
348
+ for (const entry of Array.isArray(scanRoots[provider]) ? scanRoots[provider] : []) {
349
+ if (!entry || entry.origin === "native") continue;
350
+ extras.push({ provider, ...entry });
351
+ }
352
+ }
353
+ if (extras.length > 0) {
354
+ const missing = extras.filter((entry) => !entry.exists);
355
+ const describe = (list) => list
356
+ .map((entry) => `${entry.provider} ${entry.path}${entry.error ? ` (unreadable: ${entry.error})` : ""}`)
357
+ .join(", ");
358
+ checks.push({
359
+ id: "scan_roots.extra",
360
+ status: missing.length > 0 ? "warn" : "ok",
361
+ detail: missing.length > 0
362
+ ? `${missing.length} of ${extras.length} extra scan root(s) unavailable: ${describe(missing)}`
363
+ : `${extras.length} extra scan root(s) present: ${describe(extras)}`,
364
+ critical: false,
365
+ meta: { roots: extras },
366
+ });
367
+ }
368
+ }
369
+
340
370
  return checks;
341
371
  }
342
372
 
@@ -118,6 +118,13 @@
118
118
  "cache_write": 0.167,
119
119
  "note": "See hy3-preview-agent."
120
120
  },
121
+ "qwen3.8-flash": {
122
+ "input": 0.15,
123
+ "output": 0.47,
124
+ "cache_read": 0.016,
125
+ "cache_write": 0.2,
126
+ "note": "Alibaba Model Studio Singapore (International), USD per MTok, verified 2026-09-29: https://www.alibabacloud.com/help/en/model-studio/qwen3-8-flash . Reference API estimate; other regions/providers can have different rates. Issue #715."
127
+ },
121
128
  "hy4-preview": {
122
129
  "input": 0.833,
123
130
  "output": 2.5,
@@ -277,6 +277,11 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
277
277
  }
278
278
 
279
279
  // 1. CURATED exact
280
+ // Qwen Flash's provider-qualified and dated IDs share the reference SKU.
281
+ // Keep the boundary strict: Flash-Next and Max have independent prices.
282
+ if (/(?:^|\/)qwen3[.-]8-flash(?:-\d{4}-\d{2}-\d{2})?$/.test(lower.trim()) && curated.exact?.["qwen3.8-flash"]) {
283
+ return { hit: true, source: "curated:exact", value: curated.exact["qwen3.8-flash"] };
284
+ }
280
285
  if (curated.exact && curated.exact[lookupModel]) {
281
286
  return { hit: true, source: "curated:exact", value: curated.exact[lookupModel] };
282
287
  }