tokentracker-cli 0.93.3 → 0.93.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 (105) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dashboard/dist/assets/{AchievementBadge-Bl3bWjZw.js → AchievementBadge-td60VwNO.js} +1 -1
  4. package/dashboard/dist/assets/{AchievementsPage-B6FrDjg8.js → AchievementsPage-C3yKXgId.js} +2 -2
  5. package/dashboard/dist/assets/{AchievementsSection-D5TJtcI5.js → AchievementsSection-XM39rQkD.js} +1 -1
  6. package/dashboard/dist/assets/{BadgeDetailModal-BFlrt34z.js → BadgeDetailModal-2TgBBPmw.js} +1 -1
  7. package/dashboard/dist/assets/{BotAnimated-KONFZbQT.js → BotAnimated-CG6_x0d2.js} +1 -1
  8. package/dashboard/dist/assets/{Card-DDZHhXlQ.js → Card-BUnCudVB.js} +1 -1
  9. package/dashboard/dist/assets/{ClawdAnimated-DMw54QJ_.js → ClawdAnimated-CNRu6v7I.js} +2 -2
  10. package/dashboard/dist/assets/{CommandPalette-CxVv0uo_.js → CommandPalette-D5vVqYkW.js} +1 -1
  11. package/dashboard/dist/assets/{CommunityStatsModal-CR2MuFo3.js → CommunityStatsModal-D0V4iTI6.js} +1 -1
  12. package/dashboard/dist/assets/{ConfirmModal-B-jvLsxn.js → ConfirmModal-CzxTtnXo.js} +1 -1
  13. package/dashboard/dist/assets/{Controls-DXA3Zxx0.js → Controls-nR5gzX__.js} +1 -1
  14. package/dashboard/dist/assets/{DashboardPage-0jQPu59U.js → DashboardPage-CpHXkuZn.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-CMR7jT8-.js → DevicePage-Emw9iVMd.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-B1j73B1g.js → DialogClose-Bh-pvxsM.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-DJ-RHxIt.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-eIgptOU-.js → DialogTitle-BG3ngtew.js} +1 -1
  19. package/dashboard/dist/assets/{FadeIn-B8aisUg4.js → FadeIn-DOEfAW5p.js} +1 -1
  20. package/dashboard/dist/assets/{HeaderGithubStar-B7MEGODn.js → HeaderGithubStar-DslGxMu4.js} +1 -1
  21. package/dashboard/dist/assets/{HoverTooltip-DfBzMfW7.js → HoverTooltip-O2OKCBc3.js} +1 -1
  22. package/dashboard/dist/assets/{IpCheckPage-BBvSL0Zz.js → IpCheckPage-Dv6LbNU_.js} +1 -1
  23. package/dashboard/dist/assets/{LandingPage-CcX03ndP.js → LandingPage-BBfteqDF.js} +2 -2
  24. package/dashboard/dist/assets/{LeaderboardAvatar-Dk4UJx-j.js → LeaderboardAvatar-Ct6QagwN.js} +1 -1
  25. package/dashboard/dist/assets/{LeaderboardPage-CcIp7sjv.js → LeaderboardPage-CSbIdSm7.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProfileModal-DzXfmNhy.js → LeaderboardProfileModal-BuUWkmBN.js} +3 -3
  27. package/dashboard/dist/assets/{LeaderboardProfilePage-18ijOFN-.js → LeaderboardProfilePage-fVnV0Lo-.js} +1 -1
  28. package/dashboard/dist/assets/{LimitsPage-ZeRD1J97.js → LimitsPage-BHkkA3xN.js} +1 -1
  29. package/dashboard/dist/assets/{LocalOnlyNotice-DQ_wPyho.js → LocalOnlyNotice-BLlrqreb.js} +1 -1
  30. package/dashboard/dist/assets/{LoginCard-DX5-Bxhr.js → LoginCard-CKV1uACo.js} +1 -1
  31. package/dashboard/dist/assets/{LoginModal-_Rt3qUyK.js → LoginModal-RCpQtFuG.js} +1 -1
  32. package/dashboard/dist/assets/{LoginPage-CvYcDpeR.js → LoginPage-vMV2kpef.js} +1 -1
  33. package/dashboard/dist/assets/{PetPage-D0D4mRwd.js → PetPage-CTgwpnsA.js} +1 -1
  34. package/dashboard/dist/assets/{PopoverPopup-Bz3yRvv6.js → PopoverPopup-Bddr9R6x.js} +1 -1
  35. package/dashboard/dist/assets/{ProviderIcon-PauvxgNb.js → ProviderIcon-BeXu2voQ.js} +1 -1
  36. package/dashboard/dist/assets/{ResetPasswordPage-BiC1Us4d.js → ResetPasswordPage-CilGTQWZ.js} +1 -1
  37. package/dashboard/dist/assets/{SegmentedControl-bW8MsUDI.js → SegmentedControl-r5MP3Uyx.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BSpIj6-K.js → Select-BBUIbPTj.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-taYIkVIw.js → SelectItemText-CbuN7Sui.js} +1 -1
  40. package/dashboard/dist/assets/{ServiceStatusPage-GMkqBtrt.js → ServiceStatusPage-WgmQviZa.js} +1 -1
  41. package/dashboard/dist/assets/{SessionsPage-QydYL4yt.js → SessionsPage-BDkdXQhW.js} +1 -1
  42. package/dashboard/dist/assets/{SettingsPage-BFvCcos0.js → SettingsPage-CsF8cG7I.js} +2 -2
  43. package/dashboard/dist/assets/{SkillsPage-CKci6NM2.js → SkillsPage-CVGra4ar.js} +1 -1
  44. package/dashboard/dist/assets/{TokenGalaxy-DMof07_u.js → TokenGalaxy-B0kFIG0N.js} +1 -1
  45. package/dashboard/dist/assets/{ToolbarRootContext-CAsiI0PJ.js → ToolbarRootContext-BlWkiH2k.js} +1 -1
  46. package/dashboard/dist/assets/{TrendMonitor-Dj2Uxs9x.js → TrendMonitor-C18_ADhU.js} +1 -1
  47. package/dashboard/dist/assets/{WidgetsPage-DOr-nMn1.js → WidgetsPage-Ck7L4vBj.js} +1 -1
  48. package/dashboard/dist/assets/{WrappedPage-BZUv1rX5.js → WrappedPage-Bm5HMj3a.js} +1 -1
  49. package/dashboard/dist/assets/{__vite-browser-external-WKiEWJ30.js → __vite-browser-external-BCyVFzdP.js} +15 -15
  50. package/dashboard/dist/assets/{agent-logos-1FaE1SqE.js → agent-logos-PZX7lWJo.js} +1 -1
  51. package/dashboard/dist/assets/{arrow-up-right-Bs5oicPH.js → arrow-up-right-DPSDEZPh.js} +1 -1
  52. package/dashboard/dist/assets/{check-Be2PwUMS.js → check-GGrgivnu.js} +1 -1
  53. package/dashboard/dist/assets/{chevron-right-DH93vlVo.js → chevron-right-CmSOUizu.js} +1 -1
  54. package/dashboard/dist/assets/{copy-xOLhiaQa.js → copy-ChMyV-hy.js} +1 -1
  55. package/dashboard/dist/assets/{download-qUjPQH7V.js → download-DGUkE9Jc.js} +1 -1
  56. package/dashboard/dist/assets/{external-link-D0nLaYYu.js → external-link-B_W-UDpi.js} +1 -1
  57. package/dashboard/dist/assets/{flame-DFEEnwot.js → flame-C9ccP-R8.js} +1 -1
  58. package/dashboard/dist/assets/{format-tokens-BPq1moPg.js → format-tokens-CkXJmbVP.js} +1 -1
  59. package/dashboard/dist/assets/{icons-DxM91e9c.js → icons-D37cRKGk.js} +1 -1
  60. package/dashboard/dist/assets/{index-CEl88_eC.js → index-C-_fimm3.js} +1 -1
  61. package/dashboard/dist/assets/{index-C8Sb5l3M.js → index-C9OzYqU0.js} +1 -1
  62. package/dashboard/dist/assets/{index-BEi52jGG.js → index-Dt6WIU71.js} +1 -1
  63. package/dashboard/dist/assets/{info-C43iVFmY.js → info-C3-0C2W0.js} +1 -1
  64. package/dashboard/dist/assets/{limits-providers-xNF55jgX.js → limits-providers-DB1Zr3WN.js} +1 -1
  65. package/dashboard/dist/assets/{link-2-CfarpF4W.js → link-2-DqNEbzji.js} +1 -1
  66. package/dashboard/dist/assets/{loader-circle-CIKyaEoN.js → loader-circle-BYoKm-f5.js} +1 -1
  67. package/dashboard/dist/assets/{maximize-2-DlTJ_PyX.js → maximize-2-lVQDTDqs.js} +1 -1
  68. package/dashboard/dist/assets/{modifiers.esm-CXV6l89k.js → modifiers.esm-Bww85WOd.js} +1 -1
  69. package/dashboard/dist/assets/module-D2D5r9gC.js +3 -0
  70. package/dashboard/dist/assets/{monitor-smartphone-6dCO7HCP.js → monitor-smartphone--KubNwvP.js} +1 -1
  71. package/dashboard/dist/assets/{provider-display-DNEUispM.js → provider-display-COV_Ft4A.js} +1 -1
  72. package/dashboard/dist/assets/{react-us685QUG.js → react-lNnu0y7U.js} +1 -1
  73. package/dashboard/dist/assets/{search-NcbnjXYq.js → search-CpHNMxpD.js} +1 -1
  74. package/dashboard/dist/assets/{skills-api-CFSZ441M.js → skills-api-Cf2qRQ1h.js} +1 -1
  75. package/dashboard/dist/assets/{sortable.esm-DiFLdkJ7.js → sortable.esm-DWdlF_7j.js} +1 -1
  76. package/dashboard/dist/assets/{store-B-d1Nz34.js → store-DJQqZJtg.js} +1 -1
  77. package/dashboard/dist/assets/{terminal-D3L94ngT.js → terminal-Cban5byW.js} +1 -1
  78. package/dashboard/dist/assets/{trash-2-esrIc05L.js → trash-2-nelaUHSh.js} +1 -1
  79. package/dashboard/dist/assets/{use-community-stats-C4nPlCzJ.js → use-community-stats-CBupXLW7.js} +1 -1
  80. package/dashboard/dist/assets/{use-limits-display-prefs-iXm2ziaO.js → use-limits-display-prefs-B4y_kzcd.js} +1 -1
  81. package/dashboard/dist/assets/{use-ordered-list-DU8WcVsn.js → use-ordered-list-DEV58tGc.js} +1 -1
  82. package/dashboard/dist/assets/use-reduced-motion-CRf4pBTt.js +1 -0
  83. package/dashboard/dist/assets/{use-session-efficiency-pref-CpGicS2y.js → use-session-efficiency-pref-wSk1wV6R.js} +1 -1
  84. package/dashboard/dist/assets/{use-transform-WxJRtugr.js → use-transform-NKavwYrX.js} +1 -1
  85. package/dashboard/dist/assets/{use-usage-limits-55w4KAqE.js → use-usage-limits-Blr1HBCj.js} +1 -1
  86. package/dashboard/dist/assets/{useCurrency-C6HKlFA6.js → useCurrency-B2704dMx.js} +1 -1
  87. package/dashboard/dist/assets/{useScrollLock-CZ1Na38Y.js → useScrollLock-BuPQ8T9m.js} +1 -1
  88. package/dashboard/dist/assets/{useTokenFormat-BR7kJQ6x.js → useTokenFormat-DPxAFuMj.js} +1 -1
  89. package/dashboard/dist/assets/{zap-BcwbN9j5.js → zap-BgtTdM4q.js} +1 -1
  90. package/dashboard/dist/index.html +1 -1
  91. package/dashboard/dist/ip-check.html +1 -1
  92. package/dashboard/dist/leaderboard.html +1 -1
  93. package/dashboard/dist/share.html +1 -1
  94. package/package.json +1 -1
  95. package/src/commands/serve.js +115 -4
  96. package/src/commands/status.js +3 -3
  97. package/src/commands/sync.js +146 -3
  98. package/src/lib/opencode-go-limits.js +85 -30
  99. package/src/lib/pricing/seed-snapshot.json +1 -1
  100. package/src/lib/rollout.js +372 -29
  101. package/src/lib/trae-cn-config.js +11 -2
  102. package/src/lib/usage-limits.js +119 -1
  103. package/dashboard/dist/assets/DialogDescription-ZS29ikF4.js +0 -1
  104. package/dashboard/dist/assets/module-CV1mYFsv.js +0 -3
  105. package/dashboard/dist/assets/use-reduced-motion-DDhy4yg_.js +0 -1
@@ -2698,7 +2698,8 @@ async function parseOpencodeMessageFile({
2698
2698
  };
2699
2699
  }
2700
2700
 
2701
- const model = normalizeModelInput(msg?.modelID || msg?.model || msg?.modelId) || DEFAULT_MODEL;
2701
+ const { modelId: fileModelId } = normalizeOpencodeModelFields(msg);
2702
+ const model = fileModelId || DEFAULT_MODEL;
2702
2703
  const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
2703
2704
  addTotals(bucket.totals, delta);
2704
2705
  touchedBuckets.add(bucketKey(source, model, bucketStart));
@@ -3289,8 +3290,11 @@ function deriveOpencodeMessageFingerprint({ msg, totals, source }) {
3289
3290
  const created = coerceEpochMs(msg?.time?.created) || 0;
3290
3291
  const completed = coerceEpochMs(msg?.time?.completed) || 0;
3291
3292
  if (!created && !completed) return null;
3292
- const model = normalizeModelInput(msg?.modelID || msg?.model || msg?.modelId) || DEFAULT_MODEL;
3293
- const provider = normalizeMessageKeyPart(msg?.providerID || msg?.provider || msg?.providerId);
3293
+ // v1 keeps flat modelID/providerID strings; v2 nests them in
3294
+ // `model: { id, providerID }` normalize both (see normalizeOpencodeModelFields).
3295
+ const { modelId, providerId } = normalizeOpencodeModelFields(msg);
3296
+ const model = modelId || DEFAULT_MODEL;
3297
+ const provider = providerId;
3294
3298
  const raw = [
3295
3299
  normalizeSourceInput(source) || "opencode",
3296
3300
  created,
@@ -3402,7 +3406,8 @@ function repairCountedOpencodeForkCopy({
3402
3406
  if (!timestampMs) return false;
3403
3407
  const bucketStart = toUtcHalfHourStart(new Date(timestampMs).toISOString());
3404
3408
  if (!bucketStart) return false;
3405
- const model = normalizeModelInput(msg?.modelID || msg?.model || msg?.modelId) || DEFAULT_MODEL;
3409
+ const { modelId: repairModelId } = normalizeOpencodeModelFields(msg);
3410
+ const model = repairModelId || DEFAULT_MODEL;
3406
3411
  const counted = { ...totals, conversation_count: 1 };
3407
3412
  const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
3408
3413
  subtractTotals(bucket.totals, counted);
@@ -3635,6 +3640,33 @@ function normalizeModelInput(value) {
3635
3640
  return trimmed.length > 0 ? trimmed : null;
3636
3641
  }
3637
3642
 
3643
+ // OpenCode v1 stores the model as flat strings on every assistant message
3644
+ // (`modelID`, `providerID`), while OpenCode v2 (the `opencode2` beta, which
3645
+ // moved messages into the `session_message` table) nests them inside a single
3646
+ // object: `model: { id, providerID, variant }`. Some forks also wrote a plain
3647
+ // string into `model`. Resolve both shapes to { modelId, providerId } so
3648
+ // bucket keys and fork fingerprints stay identical across versions.
3649
+ function normalizeOpencodeModelFields(msg) {
3650
+ const directModel =
3651
+ normalizeModelInput(msg?.modelID) ||
3652
+ normalizeModelInput(typeof msg?.model === "string" ? msg.model : null) ||
3653
+ normalizeModelInput(msg?.modelId);
3654
+ if (directModel) {
3655
+ return {
3656
+ modelId: directModel,
3657
+ providerId: normalizeMessageKeyPart(msg?.providerID || msg?.provider || msg?.providerId),
3658
+ };
3659
+ }
3660
+ const nested = msg?.model;
3661
+ if (nested && typeof nested === "object") {
3662
+ return {
3663
+ modelId: normalizeModelInput(nested.id),
3664
+ providerId: normalizeMessageKeyPart(nested.providerID),
3665
+ };
3666
+ }
3667
+ return { modelId: null, providerId: "" };
3668
+ }
3669
+
3638
3670
  async function resolveProjectMetaForPath(startDir, cache) {
3639
3671
  if (!startDir || typeof startDir !== "string") return null;
3640
3672
  if (cache && cache.has(startDir)) return cache.get(startDir);
@@ -4298,12 +4330,79 @@ async function walkOpencodeMessages(dir, out) {
4298
4330
  }
4299
4331
 
4300
4332
  // ---------------------------------------------------------------------------
4301
- // OpenCode SQLite DB reader (v1.2+ stores messages in opencode.db)
4333
+ // OpenCode SQLite DB reader
4334
+ //
4335
+ // Four real database shapes exist in the wild (see PR #501 / opencode2):
4336
+ // A pure v1: `message`(data) + `session_message`(empty) [+ `session`]
4337
+ // B beta-17887: `session_message`(data) + `session_v2`
4338
+ // C upstream v2: `session_message`(data) + `session`
4339
+ // D transitional: `message`(old data) + `session_message`(new data)
4340
+ //
4341
+ // The reader must serve all four without firing a doomed SQL at type A (an
4342
+ // empty `session_message` table still EXISTS — querying it with a JOIN against
4343
+ // `session_v2` that does not exist throws "no such table" every sync, and the
4344
+ // old code silently swallowed that error). The combined probe below answers
4345
+ // both "is there v2 data?" and "which session table exists?" in one round-trip.
4302
4346
  // ---------------------------------------------------------------------------
4303
4347
 
4348
+ const OPENCODE_DB_V1_MESSAGE_SQL =
4349
+ `SELECT id, session_id, time_updated, data FROM message ` +
4350
+ `WHERE json_extract(data, '$.role') = 'assistant' ORDER BY time_created ASC`;
4351
+
4352
+ // Build the v2 query. When a session table exists the LEFT JOIN restores the
4353
+ // project directory for downstream attribution; when it does not (type B
4354
+ // without session_v2, or an exotic fork) the join and directory column are
4355
+ // omitted and tokens are counted as-is.
4356
+ function buildV2Sql(sessionTable) {
4357
+ const joinClause = sessionTable
4358
+ ? `LEFT JOIN ${sessionTable} s ON s.id = sm.session_id `
4359
+ : "";
4360
+ const directorySelect = sessionTable
4361
+ ? `s.directory AS directory, `
4362
+ : "";
4363
+ return (
4364
+ `SELECT sm.id AS id, sm.session_id AS session_id, sm.time_updated AS time_updated, ` +
4365
+ `${directorySelect}sm.data AS data ` +
4366
+ `FROM session_message sm ${joinClause}` +
4367
+ `WHERE sm.type = 'assistant' ORDER BY sm.time_created ASC`
4368
+ );
4369
+ }
4370
+
4371
+ // One combined probe: hasRows (is there any data in session_message?) plus
4372
+ // sessionTable (which session table exists, session_v2 preferred over session
4373
+ // via DESC). Returns null when the probe itself fails — callers treat that as
4374
+ // v1-only, matching the pre-existing silent-degradation contract.
4375
+ function detectOpencodeMessageLayout(dbPath, sqliteOptions = {}) {
4376
+ let rows;
4377
+ try {
4378
+ rows = readSqliteJsonRows(
4379
+ dbPath,
4380
+ `SELECT (SELECT 1 FROM session_message LIMIT 1) AS hasRows,
4381
+ (SELECT name FROM sqlite_master WHERE name IN ('session','session_v2') ORDER BY name DESC LIMIT 1) AS sessionTable`,
4382
+ { label: "OpenCode", timeout: 10_000, maxBuffer: 1024 * 1024, ...sqliteOptions },
4383
+ );
4384
+ } catch (_e) {
4385
+ return null;
4386
+ }
4387
+ if (!Array.isArray(rows) || rows.length === 0) return null;
4388
+ const row = rows[0];
4389
+ return {
4390
+ hasRows: Boolean(row?.hasRows),
4391
+ sessionTable: typeof row?.sessionTable === "string" && row.sessionTable.trim()
4392
+ ? row.sessionTable.trim()
4393
+ : null,
4394
+ };
4395
+ }
4396
+
4397
+ // Shared provider resolver: v1 rows carry a top-level providerID, v2 rows nest
4398
+ // it under model.providerID. Used by the mimo/zcode discriminators so they work
4399
+ // against both generations without duplicating the resolution logic.
4400
+ function opencodeMessageProvider(data) {
4401
+ return data?.providerID || data?.model?.providerID || "";
4402
+ }
4403
+
4304
4404
  function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
4305
4405
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
4306
- const sql = `SELECT id, session_id, time_updated, data FROM message WHERE json_extract(data, '$.role') = 'assistant' ORDER BY time_created ASC`;
4307
4406
 
4308
4407
  let snapshot = null;
4309
4408
  let effectiveDbPath = dbPath;
@@ -4314,14 +4413,16 @@ function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
4314
4413
  } catch (_e) { }
4315
4414
  }
4316
4415
 
4317
- try {
4318
- const rows = readSqliteJsonRows(effectiveDbPath, sql, {
4319
- label: "OpenCode",
4320
- maxBuffer: 50 * 1024 * 1024,
4321
- timeout: 30_000,
4322
- ...sqliteOptions,
4323
- });
4324
- const out = [];
4416
+ // Parse raw SQL rows into the shared { id, sessionID, timeUpdated, data }
4417
+ // shape; optionally restore v2's session-level project directory as the
4418
+ // per-message `path.cwd` the rest of the pipeline expects.
4419
+ //
4420
+ // D-state (transitional) union: both the legacy `message` table and the new
4421
+ // `session_message` table may carry rows. Cross-table duplicates are caught
4422
+ // by the existing cursor-key + #426 fingerprint dedup in
4423
+ // parseOpencodeDbIncremental, so unioning here is safe — the downstream
4424
+ // parser collapses identical sessionID|messageID pairs before bucketing.
4425
+ const appendRows = (rows, isV2, out) => {
4325
4426
  for (const row of rows) {
4326
4427
  if (!row || typeof row.data !== "string") continue;
4327
4428
  let data;
@@ -4338,6 +4439,9 @@ function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
4338
4439
  toNonNegativeInt(tokens.output) > 0 ||
4339
4440
  toNonNegativeInt(tokens.reasoning) > 0;
4340
4441
  if (!hasTokens) continue;
4442
+ if (isV2 && typeof row.directory === "string" && row.directory.trim()) {
4443
+ data.path = { ...(typeof data.path === "object" && data.path ? data.path : {}), cwd: row.directory };
4444
+ }
4341
4445
  out.push({
4342
4446
  id: row.id || data.id,
4343
4447
  sessionID: row.session_id || data.sessionID,
@@ -4345,6 +4449,39 @@ function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
4345
4449
  data,
4346
4450
  });
4347
4451
  }
4452
+ };
4453
+
4454
+ const readGeneration = (sql) => {
4455
+ try {
4456
+ const rows = readSqliteJsonRows(effectiveDbPath, sql, {
4457
+ label: "OpenCode",
4458
+ maxBuffer: 50 * 1024 * 1024,
4459
+ timeout: 30_000,
4460
+ ...sqliteOptions,
4461
+ throwOnReadFailure: true,
4462
+ });
4463
+ return Array.isArray(rows) ? rows : [];
4464
+ } catch (_e) {
4465
+ return null; // query-level failure (e.g. wrong generation's table)
4466
+ }
4467
+ };
4468
+
4469
+ try {
4470
+ // Combined probe drives the orchestration: v1 always runs (it is the
4471
+ // baseline every database carries or degrades to), v2 runs only when the
4472
+ // probe confirms session_message has rows — this avoids firing a JOIN
4473
+ // against a non-existent session table on type-A databases.
4474
+ const probe = detectOpencodeMessageLayout(effectiveDbPath, sqliteOptions);
4475
+ const out = [];
4476
+
4477
+ const rows1 = readGeneration(OPENCODE_DB_V1_MESSAGE_SQL);
4478
+ if (rows1) appendRows(rows1, false, out);
4479
+
4480
+ if (probe?.hasRows) {
4481
+ const rows2 = readGeneration(buildV2Sql(probe.sessionTable));
4482
+ if (rows2) appendRows(rows2, true, out);
4483
+ }
4484
+
4348
4485
  return out;
4349
4486
  } finally {
4350
4487
  if (snapshot) snapshot.cleanup();
@@ -4369,7 +4506,7 @@ function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
4369
4506
  // subsumes it.
4370
4507
  function isMimoNativeMessage(data) {
4371
4508
  if (!data) return false;
4372
- const provider = String(data.providerID || "").toLowerCase();
4509
+ const provider = String(opencodeMessageProvider(data)).toLowerCase();
4373
4510
  return provider === "mimo" || provider === "xiaomi";
4374
4511
  }
4375
4512
 
@@ -4403,7 +4540,7 @@ function readMimoDbMessages(dbPath, sqliteOptions = {}) {
4403
4540
  // re-count it. Mirrors the Mimo discipline.
4404
4541
  function isZcodeNativeMessage(data) {
4405
4542
  if (!data) return false;
4406
- const provider = String(data.providerID || "").toLowerCase();
4543
+ const provider = String(opencodeMessageProvider(data)).toLowerCase();
4407
4544
  if (!provider) return false;
4408
4545
  return !(
4409
4546
  provider.includes("anthropic") ||
@@ -4412,10 +4549,195 @@ function isZcodeNativeMessage(data) {
4412
4549
  );
4413
4550
  }
4414
4551
 
4552
+ const ZCODE_NATIVE_USAGE_COLUMNS = new Set([
4553
+ "id",
4554
+ "logical_request_id",
4555
+ "attempt_index",
4556
+ "session_id",
4557
+ "provider_id",
4558
+ "model_id",
4559
+ "status",
4560
+ "started_at",
4561
+ "input_tokens",
4562
+ "output_tokens",
4563
+ "reasoning_tokens",
4564
+ "cache_creation_input_tokens",
4565
+ "cache_read_input_tokens",
4566
+ ]);
4567
+
4568
+ function detectZcodeNativeUsageLayout(dbPath, sqliteOptions = {}) {
4569
+ if (!dbPath || !fssync.existsSync(dbPath)) return null;
4570
+ let rows;
4571
+ try {
4572
+ rows = readSqliteJsonRows(
4573
+ dbPath,
4574
+ `SELECT 'model_usage' AS table_name, name FROM pragma_table_info('model_usage')
4575
+ UNION ALL
4576
+ SELECT 'session' AS table_name, name FROM pragma_table_info('session')`,
4577
+ {
4578
+ label: "ZCode",
4579
+ timeout: 10_000,
4580
+ maxBuffer: 1024 * 1024,
4581
+ ...sqliteOptions,
4582
+ throwOnReadFailure: true,
4583
+ },
4584
+ );
4585
+ } catch (_error) {
4586
+ return null;
4587
+ }
4588
+ const modelUsageColumns = new Set(
4589
+ rows
4590
+ .filter((row) => !row?.table_name || row.table_name === "model_usage")
4591
+ .map((row) => String(row?.name || "")),
4592
+ );
4593
+ if (![...ZCODE_NATIVE_USAGE_COLUMNS].every((name) => modelUsageColumns.has(name))) {
4594
+ return null;
4595
+ }
4596
+ const sessionColumns = new Set(
4597
+ rows
4598
+ .filter((row) => row?.table_name === "session")
4599
+ .map((row) => String(row?.name || "")),
4600
+ );
4601
+ return {
4602
+ hasSessionDirectory: sessionColumns.has("id") && sessionColumns.has("directory"),
4603
+ };
4604
+ }
4605
+
4606
+ function hasZcodeNativeUsageSchema(dbPath, sqliteOptions = {}) {
4607
+ if (!dbPath || !fssync.existsSync(dbPath)) return false;
4608
+ let snapshot = null;
4609
+ let effectiveDbPath = dbPath;
4610
+ if (isUncPath(dbPath)) {
4611
+ try {
4612
+ snapshot = snapshotSqliteDb(dbPath);
4613
+ effectiveDbPath = snapshot.path;
4614
+ } catch (_error) {
4615
+ // Fall through to the direct read: some UNC servers support SQLite's
4616
+ // read-only locking semantics even when the WSL bridge does not.
4617
+ }
4618
+ }
4619
+ try {
4620
+ return detectZcodeNativeUsageLayout(effectiveDbPath, sqliteOptions) !== null;
4621
+ } finally {
4622
+ if (snapshot) snapshot.cleanup();
4623
+ }
4624
+ }
4625
+
4626
+ function buildZcodeNativeUsageSql({ hasSessionDirectory }) {
4627
+ const directorySelect = hasSessionDirectory ? ", s.directory AS directory" : "";
4628
+ const directoryJoin = hasSessionDirectory
4629
+ ? " LEFT JOIN session AS s ON s.id = mu.session_id"
4630
+ : "";
4631
+ return `SELECT
4632
+ mu.id,
4633
+ mu.logical_request_id,
4634
+ mu.attempt_index,
4635
+ mu.session_id,
4636
+ mu.provider_id,
4637
+ mu.model_id,
4638
+ mu.started_at,
4639
+ mu.input_tokens,
4640
+ mu.output_tokens,
4641
+ mu.reasoning_tokens,
4642
+ mu.cache_creation_input_tokens,
4643
+ mu.cache_read_input_tokens
4644
+ ${directorySelect}
4645
+ FROM model_usage AS mu${directoryJoin}
4646
+ WHERE mu.status = 'completed'
4647
+ AND trim(mu.model_id) != ''
4648
+ AND (
4649
+ mu.input_tokens > 0 OR mu.output_tokens > 0 OR mu.reasoning_tokens > 0 OR
4650
+ mu.cache_creation_input_tokens > 0 OR mu.cache_read_input_tokens > 0
4651
+ )
4652
+ ORDER BY mu.started_at ASC, mu.id ASC`;
4653
+ }
4654
+
4655
+ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
4656
+ let snapshot = null;
4657
+ let effectiveDbPath = dbPath;
4658
+ if (isUncPath(dbPath)) {
4659
+ try {
4660
+ snapshot = snapshotSqliteDb(dbPath);
4661
+ effectiveDbPath = snapshot.path;
4662
+ } catch (_error) {
4663
+ // Preserve the existing direct-read fallback for transient snapshot
4664
+ // failures and UNC implementations that support SQLite locking.
4665
+ }
4666
+ }
4667
+
4668
+ let rows;
4669
+ try {
4670
+ const layout = detectZcodeNativeUsageLayout(effectiveDbPath, sqliteOptions);
4671
+ if (!layout) return null;
4672
+ rows = readSqliteJsonRows(effectiveDbPath, buildZcodeNativeUsageSql(layout), {
4673
+ label: "ZCode",
4674
+ maxBuffer: 50 * 1024 * 1024,
4675
+ timeout: 30_000,
4676
+ ...sqliteOptions,
4677
+ throwOnReadFailure: true,
4678
+ });
4679
+ } catch (_error) {
4680
+ return null;
4681
+ } finally {
4682
+ if (snapshot) snapshot.cleanup();
4683
+ }
4684
+
4685
+ const messages = [];
4686
+ for (const row of rows) {
4687
+ const providerID = String(row?.provider_id || "").trim();
4688
+ const modelID = String(row?.model_id || "").trim();
4689
+ const sessionID = String(row?.session_id || "").trim();
4690
+ const logicalRequestId = String(row?.logical_request_id || "").trim();
4691
+ const attemptIndex = toNonNegativeInt(row?.attempt_index);
4692
+ const id = String(row?.id || "").trim() ||
4693
+ (logicalRequestId ? `${logicalRequestId}#${attemptIndex}` : "");
4694
+ const startedAt = coerceEpochMs(row?.started_at);
4695
+ if (!providerID || !modelID || !sessionID || !id || !startedAt) continue;
4696
+
4697
+ // ZCode's persisted input/output columns are inclusive parents:
4698
+ // cache read/write are subsets of input, and reasoning is a subset of
4699
+ // output. TokenTracker stores those five columns disjointly, so split the
4700
+ // subsets here before entering the shared OpenCode parser. This preserves
4701
+ // sum(input + cache read + cache write + output + reasoning) without
4702
+ // billing either subset twice.
4703
+ const rawInput = toNonNegativeInt(row?.input_tokens);
4704
+ const rawOutput = toNonNegativeInt(row?.output_tokens);
4705
+ const cacheRead = toNonNegativeInt(row?.cache_read_input_tokens);
4706
+ const cacheWrite = toNonNegativeInt(row?.cache_creation_input_tokens);
4707
+ const reasoning = toNonNegativeInt(row?.reasoning_tokens);
4708
+
4709
+ const data = {
4710
+ id,
4711
+ sessionID,
4712
+ role: "assistant",
4713
+ providerID,
4714
+ modelID,
4715
+ time: { created: startedAt, completed: startedAt },
4716
+ tokens: {
4717
+ input: Math.max(0, rawInput - cacheRead - cacheWrite),
4718
+ output: Math.max(0, rawOutput - reasoning),
4719
+ reasoning,
4720
+ cache: {
4721
+ read: cacheRead,
4722
+ write: cacheWrite,
4723
+ },
4724
+ },
4725
+ };
4726
+ if (typeof row?.directory === "string" && row.directory.trim()) {
4727
+ data.path = { cwd: row.directory.trim() };
4728
+ }
4729
+ if (!isZcodeNativeMessage(data)) continue;
4730
+ messages.push({ id, sessionID, timeUpdated: startedAt, data });
4731
+ }
4732
+ return messages;
4733
+ }
4734
+
4415
4735
  // Read only genuine ZCode assistant messages (its own GLM models via Z.ai /
4416
4736
  // BigModel), dropping any bundled sub-agent turns. See isZcodeNativeMessage.
4417
4737
  function readZcodeDbMessages(dbPath, sqliteOptions = {}) {
4418
4738
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
4739
+ const nativeMessages = readZcodeNativeUsageMessages(dbPath, sqliteOptions);
4740
+ if (nativeMessages !== null) return nativeMessages;
4419
4741
  const all = readOpencodeDbMessages(dbPath, sqliteOptions);
4420
4742
  return all.filter((m) => isZcodeNativeMessage(m.data));
4421
4743
  }
@@ -4562,7 +4884,8 @@ async function parseOpencodeDbIncremental({
4562
4884
  continue;
4563
4885
  }
4564
4886
 
4565
- const model = normalizeModelInput(msg?.modelID || msg?.model || msg?.modelId) || DEFAULT_MODEL;
4887
+ const { modelId: dbModelId } = normalizeOpencodeModelFields(msg);
4888
+ const model = dbModelId || DEFAULT_MODEL;
4566
4889
  const bucket = getHourlyBucket(hourlyState, defaultSource, model, bucketStart);
4567
4890
  addTotals(bucket.totals, delta);
4568
4891
  touchedBuckets.add(bucketKey(defaultSource, model, bucketStart));
@@ -16614,36 +16937,55 @@ function isCjkCodePoint(code) {
16614
16937
  // (session transcripts are SQLCipher-encrypted; memory summaries carry no
16615
16938
  // token counts), so the token-count-only queue is intentionally never written
16616
16939
  // for this provider — the cloud hourly table stays clean.
16617
- function resolveTraePath(env = process.env) {
16940
+ // Ordered candidate app-dir names. The CN IDE build installs as "Trae CN" on
16941
+ // Windows; the international build installs as "TRAE SOLO". Both expose the same
16942
+ // iCubeServerData entitlement key, so either is a valid Trae IDE install.
16943
+ function traeCandidateAppDirs(env = process.env) {
16618
16944
  const override = env.TOKENTRACKER_TRAE_HOME;
16619
16945
  if (typeof override === "string" && override.trim().length > 0) {
16620
- return override.trim();
16946
+ return [override.trim()];
16621
16947
  }
16622
16948
  const home = require("node:os").homedir();
16623
16949
  if (process.platform === "darwin") {
16624
- return path.join(home, "Library", "Application Support", "TRAE SOLO");
16950
+ return [path.join(home, "Library", "Application Support", "TRAE SOLO")];
16625
16951
  }
16626
16952
  if (process.platform === "win32") {
16627
16953
  // A Windows box with no APPDATA still keeps Trae under the standard
16628
16954
  // roaming profile — falling through to the dot-dir below would point at
16629
16955
  // a location Trae never writes.
16630
- const appData = typeof env.APPDATA === "string" ? env.APPDATA.trim() : "";
16631
- return appData
16632
- ? path.join(appData, "TRAE SOLO")
16633
- : path.join(home, "AppData", "Roaming", "TRAE SOLO");
16956
+ const appData =
16957
+ typeof env.APPDATA === "string" && env.APPDATA.trim()
16958
+ ? env.APPDATA.trim()
16959
+ : path.join(home, "AppData", "Roaming");
16960
+ return [
16961
+ path.join(appData, "TRAE SOLO"),
16962
+ path.join(appData, "Trae CN"),
16963
+ ];
16634
16964
  }
16635
16965
  // Linux and friends: TRAE SOLO ships official builds for macOS/Windows
16636
16966
  // only, so there is no verified app-data layout to default to. Fall back to
16637
16967
  // a deterministic home-dir path (best-effort detection);
16638
16968
  // TOKENTRACKER_TRAE_HOME always wins for unusual installs.
16639
- return path.join(home, ".trae-solo");
16969
+ return [path.join(home, ".trae-solo")];
16970
+ }
16971
+
16972
+ // Best-effort: the first candidate that exists on disk (used for display/init
16973
+ // detection). When no install is present, returns the primary candidate so
16974
+ // callers still get a deterministic path to report.
16975
+ function resolveTraePath(env = process.env) {
16976
+ const candidates = traeCandidateAppDirs(env);
16977
+ for (const dir of candidates) {
16978
+ try { if (fssync.existsSync(dir)) return dir; } catch (_error) {}
16979
+ }
16980
+ return candidates.length ? candidates[0] : null;
16640
16981
  }
16641
16982
 
16642
16983
  function resolveTraeStoragePath(env = process.env) {
16643
- const traHome = resolveTraePath(env);
16644
- if (!traHome) return null;
16645
- const p = path.join(traHome, "User", "globalStorage", "storage.json");
16646
- return fssync.existsSync(p) ? p : null;
16984
+ for (const dir of traeCandidateAppDirs(env)) {
16985
+ const p = path.join(dir, "User", "globalStorage", "storage.json");
16986
+ try { if (fssync.existsSync(p)) return p; } catch (_error) {}
16987
+ }
16988
+ return null;
16647
16989
  }
16648
16990
 
16649
16991
  /**
@@ -17895,6 +18237,7 @@ module.exports = {
17895
18237
  readOpencodeDbMessages,
17896
18238
  readMimoDbMessages,
17897
18239
  readZcodeDbMessages,
18240
+ hasZcodeNativeUsageSchema,
17898
18241
  resolveQoderDbPath,
17899
18242
  resolveQoderDbPaths,
17900
18243
  resolveQoderCnDbPaths,
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * TRAE Work CN (国内版) config + usage fetch. Phase A: macOS + test-injected
4
+ * TRAE Work CN (国内版) config + usage fetch. Phase A: macOS + Windows + test-injected
5
5
  * paths only. Resolves the CN home, reads the `iCubeAuthInfo://icube.cloudide`
6
6
  * blob from User/globalStorage/storage.json (plaintext JSON or Base64 tc v5),
7
7
  * and talks to the TRAE CN usage API. JWTs / refresh tokens are never
@@ -85,7 +85,16 @@ function resolveTraeCnHome({ env = process.env, home = os.homedir(), platform =
85
85
  if (platform === "darwin") {
86
86
  return path.join(home, "Library", "Application Support", TRAE_CN_APP_DIR);
87
87
  }
88
- // ponytail: this phase is macOS + test-injected paths only.
88
+ if (platform === "win32") {
89
+ // Windows keeps the CN Work app under the roaming profile, mirroring the
90
+ // macOS Application Support layout (APPDATA/TRAE SOLO CN/User/...).
91
+ const appData =
92
+ typeof env.APPDATA === "string" && env.APPDATA.trim()
93
+ ? env.APPDATA.trim()
94
+ : path.join(home, "AppData", "Roaming");
95
+ return path.join(appData, TRAE_CN_APP_DIR);
96
+ }
97
+ // No verified CN Work app-data layout on other platforms yet.
89
98
  return null;
90
99
  }
91
100