tokentracker-cli 1.3.16 → 1.4.2

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 (145) 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 +5 -4
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{AchievementBadge-D5sRTMmp.js → AchievementBadge-WGbdoboF.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CaiCfchd.js → AchievementsPage-BvybbsXv.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-D6dTxdUd.js → AchievementsSection-ChNzTFSR.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-C0UrWb9m.js → BadgeDetailModal-DhI9Ejz3.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-CgpBB5FZ.js → BotAnimated-Bch0Yxej.js} +1 -1
  11. package/dashboard/dist/assets/{Card-CuwBiIYz.js → Card-DWdL7d1T.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-BNCnBR0h.js → CloudCheckoutPage-BjGoOz6P.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-CuYXUQnv.js → CloudPage-Btb6a97x.js} +2 -2
  14. package/dashboard/dist/assets/{CommandPalette-CV6a_7yK.js → CommandPalette-DwP3k7LE.js} +1 -1
  15. package/dashboard/dist/assets/{CommunityStatsModal-Dw-64Tig.js → CommunityStatsModal-mzqLxpwE.js} +1 -1
  16. package/dashboard/dist/assets/{ConfirmModal-CoFO1Nas.js → ConfirmModal-Dm76q4EX.js} +1 -1
  17. package/dashboard/dist/assets/{Controls-DB9eQ_rg.js → Controls-ClZjOBoD.js} +1 -1
  18. package/dashboard/dist/assets/DashboardPage-Doc8f4NL.js +1 -0
  19. package/dashboard/dist/assets/{DateRangePopover-DgZEnn4X.js → DateRangePopover-vfTUePPg.js} +1 -1
  20. package/dashboard/dist/assets/DevicePage-Jjg5q0C2.js +1 -0
  21. package/dashboard/dist/assets/{DialogClose-BR9QrB34.js → DialogClose-Ci5mPZ6X.js} +1 -1
  22. package/dashboard/dist/assets/DialogDescription-Di5ev5K6.js +1 -0
  23. package/dashboard/dist/assets/DialogTitle-DI21oZxH.js +1 -0
  24. package/dashboard/dist/assets/{ElectricLogo-nrlBy-l9.js → ElectricLogo-DfeZBwPZ.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-CyJJ3l_P.js → FadeIn-C7SIILTc.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-C7BX7loS.js → HeaderGithubStar-RStLbadn.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-KGluh6sO.js → HoverTooltip-DI7uAp1_.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-Ci2ybTDg.js → IpCheckPage-DTZNTJVh.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-C9N17IpV.js → LandingPage-Df5scNlw.js} +2 -2
  30. package/dashboard/dist/assets/{LeaderboardPage-Cco7iFen.js → LeaderboardPage-Ck3SqtnI.js} +2 -2
  31. package/dashboard/dist/assets/{LeaderboardProBadge-tmm5NN8p.js → LeaderboardProBadge-CGx6m7R6.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-WhjsvZ9Q.js → LeaderboardProfileModal-DuP7rgfv.js} +3 -3
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-Ba3oVu5z.js → LeaderboardProfilePage-B5qUJkZX.js} +1 -1
  34. package/dashboard/dist/assets/{LimitsPage-CUa46XJy.js → LimitsPage-Bv_Lcvcz.js} +1 -1
  35. package/dashboard/dist/assets/{LocalOnlyNotice-BSewrWlY.js → LocalOnlyNotice-Cg4y02JM.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-CUWn0Tz4.js → LoginCard-Wz0uJKXx.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-UKk3xCsV.js → LoginModal-C13v_Gs0.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-BhxiWgc5.js → LoginPage-COH9xXtd.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-BRyxC-Iu.js → PetPage-BWCnURub.js} +2 -2
  40. package/dashboard/dist/assets/PopoverPopup-Dw811N-_.js +1 -0
  41. package/dashboard/dist/assets/{ProviderIcon-htNu1RTN.js → ProviderIcon-CHxhYzK-.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-DawMNiM1.js → ResetPasswordPage-Ll6jshqg.js} +1 -1
  43. package/dashboard/dist/assets/Select-BbooQ3sa.js +1 -0
  44. package/dashboard/dist/assets/SelectItemText-DBiW70wi.js +1 -0
  45. package/dashboard/dist/assets/{SelfHostPage-BMCMrt5I.js → SelfHostPage-BMHJxz5q.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-e_7xC0RY.js → ServiceStatusPage-D6LH0ac-.js} +1 -1
  47. package/dashboard/dist/assets/{SessionsPage-Xr2FJa6E.js → SessionsPage-Mrer6v17.js} +2 -2
  48. package/dashboard/dist/assets/SettingsPage-D_7P_y_l.js +5 -0
  49. package/dashboard/dist/assets/SkillsPage-Ka7xm-1v.js +1 -0
  50. package/dashboard/dist/assets/{TokenGalaxy--6ijNK6P.js → TokenGalaxy-BEURDPFq.js} +1 -1
  51. package/dashboard/dist/assets/TooltipPopup-ZiuFFARC.js +1 -0
  52. package/dashboard/dist/assets/{TrendMonitor-DjIZyzxp.js → TrendMonitor-H73lyGEf.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-CzCqGbb4.js → WidgetsPage-_7iCvyTk.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-B0RMtpvr.js → WrappedPage-DEXNb6-k.js} +1 -1
  55. package/dashboard/dist/assets/__vite-browser-external-BkNP5Ecj.js +45 -0
  56. package/dashboard/dist/assets/{agent-logos-CSlyeabY.js → agent-logos-OVBc_0cF.js} +1 -1
  57. package/dashboard/dist/assets/{arrow-left-DaqMiXTO.js → arrow-left-DDphsclm.js} +1 -1
  58. package/dashboard/dist/assets/{arrow-right-B1p9R8vH.js → arrow-right-B1aNq0Ki.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-up-right-CK_9HuEh.js → arrow-up-right-yTM9azz_.js} +1 -1
  60. package/dashboard/dist/assets/{calendar-days-BVyCx4dJ.js → calendar-days-D22ithNF.js} +1 -1
  61. package/dashboard/dist/assets/{check-DIfCVsPJ.js → check-Dk7tknvE.js} +1 -1
  62. package/dashboard/dist/assets/{chevron-down-DrWxRBXX.js → chevron-down-DdyEzQ4f.js} +1 -1
  63. package/dashboard/dist/assets/{cloud-checkout-DOxBG7oS.js → cloud-checkout-BJjbipnH.js} +1 -1
  64. package/dashboard/dist/assets/composite-DEqP00XV.js +1 -0
  65. package/dashboard/dist/assets/{download-CEXAmejX.js → download-C9ixNyzn.js} +1 -1
  66. package/dashboard/dist/assets/{external-link-tDBZy6QP.js → external-link-DL4HSsLw.js} +1 -1
  67. package/dashboard/dist/assets/{flame-86y9Xf0g.js → flame-DqNH65nB.js} +1 -1
  68. package/dashboard/dist/assets/{format-tokens-BNN9k5mV.js → format-tokens-CyVjKz4I.js} +1 -1
  69. package/dashboard/dist/assets/{gift-BEOvq2zc.js → gift-ChJQQe_G.js} +1 -1
  70. package/dashboard/dist/assets/{host-mode-UjbNKxFB.js → host-mode-DpUDpoOz.js} +1 -1
  71. package/dashboard/dist/assets/{icons-DJmZD0si.js → icons-CJiHeOaU.js} +1 -1
  72. package/dashboard/dist/assets/{index-BwUwnqE0.js → index-72LQsAaf.js} +1 -1
  73. package/dashboard/dist/assets/{index-B8kizz-s.js → index-BucEg3fu.js} +1 -1
  74. package/dashboard/dist/assets/{index-Ci3CZ-2g.js → index-CrFlTSra.js} +1 -1
  75. package/dashboard/dist/assets/{info-ynVg8RzK.js → info-DReYqxC7.js} +1 -1
  76. package/dashboard/dist/assets/{link-2-B9SLPcBg.js → link-2-kIZbhK2S.js} +1 -1
  77. package/dashboard/dist/assets/{loader-circle-FTE9k7Sk.js → loader-circle-1An-X85y.js} +1 -1
  78. package/dashboard/dist/assets/{modifiers.esm-B4babCm_.js → modifiers.esm-FoVheEKT.js} +1 -1
  79. package/dashboard/dist/assets/module-DOCYJaxl.js +3 -0
  80. package/dashboard/dist/assets/{monitor-smartphone-CNzgbo-M.js → monitor-smartphone-Ch6lr_P2.js} +1 -1
  81. package/dashboard/dist/assets/{play-9W4Ua21o.js → play-CbJ6ZF1K.js} +1 -1
  82. package/dashboard/dist/assets/{plus-BA85lApI.js → plus-C-5ID3a9.js} +1 -1
  83. package/dashboard/dist/assets/{provider-display-D1ETarsz.js → provider-display-BFzRtoL2.js} +2 -2
  84. package/dashboard/dist/assets/{react-VsHxeqq7.js → react-D4CR_fLQ.js} +1 -1
  85. package/dashboard/dist/assets/safePolygon-DP21Ibqj.js +1 -0
  86. package/dashboard/dist/assets/{search-Svi--7CN.js → search-CaznkY7U.js} +1 -1
  87. package/dashboard/dist/assets/{server-CgHdX6TE.js → server-CUOmC6xZ.js} +1 -1
  88. package/dashboard/dist/assets/{shield-check-Cjv-T4aT.js → shield-check-Dr2KBMmA.js} +1 -1
  89. package/dashboard/dist/assets/{skills-api-Cn8hEYL4.js → skills-api-BAz_fblS.js} +1 -1
  90. package/dashboard/dist/assets/{sortable.esm-g3UcmvFU.js → sortable.esm-knEfm5hE.js} +1 -1
  91. package/dashboard/dist/assets/store-CL97jcij.js +1 -0
  92. package/dashboard/dist/assets/{terminal-o0jvDfI0.js → terminal-EjFyreWj.js} +1 -1
  93. package/dashboard/dist/assets/{trash-0cqpTyN6.js → trash-ByqaZXkQ.js} +1 -1
  94. package/dashboard/dist/assets/{use-community-stats-BFgORaJS.js → use-community-stats-Bs4RDJZE.js} +1 -1
  95. package/dashboard/dist/assets/{use-latest-request-guard-9eZP_jA5.js → use-latest-request-guard-CG3Ir1cP.js} +1 -1
  96. package/dashboard/dist/assets/{use-limits-display-prefs-BjO3hsmw.js → use-limits-display-prefs-tACgCxeR.js} +1 -1
  97. package/dashboard/dist/assets/{use-native-settings-YKn-htVo.js → use-native-settings-CWjXyJXi.js} +1 -1
  98. package/dashboard/dist/assets/{use-ordered-list-D5h4UwhF.js → use-ordered-list-DsuC9JQa.js} +1 -1
  99. package/dashboard/dist/assets/use-reduced-motion-CwFguzkm.js +1 -0
  100. package/dashboard/dist/assets/{use-session-efficiency-pref-BShLuIbk.js → use-session-efficiency-pref-NeoJE8BL.js} +1 -1
  101. package/dashboard/dist/assets/{use-transform-DVZHJPVD.js → use-transform-BWGKOsya.js} +1 -1
  102. package/dashboard/dist/assets/{use-usage-limits-BhJbW76K.js → use-usage-limits-CFWFNwHS.js} +1 -1
  103. package/dashboard/dist/assets/{useCurrency-BmEbDVaa.js → useCurrency-B_C4_P5x.js} +1 -1
  104. package/dashboard/dist/assets/usePositioner-B0L5_o38.js +1 -0
  105. package/dashboard/dist/assets/{useTokenFormat-CBlBP2aA.js → useTokenFormat-DH1GWHSU.js} +1 -1
  106. package/dashboard/dist/assets/{zap-cML2-Nx4.js → zap-Chv1p7pS.js} +1 -1
  107. package/dashboard/dist/index.html +13 -11
  108. package/dashboard/dist/ip-check.html +1 -1
  109. package/dashboard/dist/leaderboard.html +1 -1
  110. package/dashboard/dist/llms.txt +3 -2
  111. package/dashboard/dist/share.html +1 -1
  112. package/package.json +3 -3
  113. package/src/commands/doctor.js +15 -1
  114. package/src/commands/init.js +9 -0
  115. package/src/commands/status.js +21 -3
  116. package/src/commands/sync.js +59 -4
  117. package/src/lib/cloud-device-token.js +77 -10
  118. package/src/lib/cloud-usage-reset.js +115 -0
  119. package/src/lib/diagnostics.js +5 -2
  120. package/src/lib/kimi-model-repair.js +173 -0
  121. package/src/lib/kimi-session.js +216 -0
  122. package/src/lib/local-api.js +46 -1
  123. package/src/lib/pricing/index.js +2 -1
  124. package/src/lib/pricing/seed-snapshot.json +1 -1
  125. package/src/lib/qoder-cn-cli-limits.js +9 -7
  126. package/src/lib/qoder-limits.js +45 -15
  127. package/src/lib/rollout.js +408 -4
  128. package/src/lib/session-analytics.js +47 -9
  129. package/dashboard/dist/assets/DashboardPage-sZKijU6h.js +0 -1
  130. package/dashboard/dist/assets/DevicePage-YCnJnsmz.js +0 -1
  131. package/dashboard/dist/assets/DialogDescription-Cx1R3WT7.js +0 -1
  132. package/dashboard/dist/assets/DialogTitle-ck8f1qeP.js +0 -1
  133. package/dashboard/dist/assets/PopoverPopup-D-Tpl294.js +0 -1
  134. package/dashboard/dist/assets/Select-6WChWtqK.js +0 -1
  135. package/dashboard/dist/assets/SelectItemText-DRmxMwo5.js +0 -1
  136. package/dashboard/dist/assets/SettingsPage-Bxq2Mhxz.js +0 -5
  137. package/dashboard/dist/assets/SkillsPage-CEIBW63c.js +0 -1
  138. package/dashboard/dist/assets/TooltipPopup--TLYUg0c.js +0 -1
  139. package/dashboard/dist/assets/__vite-browser-external-BsOjTB8Q.js +0 -45
  140. package/dashboard/dist/assets/composite-W1R1XwoE.js +0 -1
  141. package/dashboard/dist/assets/module-BNnaKM__.js +0 -3
  142. package/dashboard/dist/assets/safePolygon-CCDOsx-z.js +0 -1
  143. package/dashboard/dist/assets/store-D2gn9bSV.js +0 -1
  144. package/dashboard/dist/assets/use-reduced-motion-C83gmg0F.js +0 -1
  145. package/dashboard/dist/assets/usePositioner-nY0YNa-6.js +0 -1
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs/promises");
4
+ const path = require("node:path");
5
+ const crypto = require("node:crypto");
6
+
7
+ const KIMI_MODEL_REPAIR_KEY = "kimiModelAttribution_2026_10";
8
+ const TOKEN_FIELDS = ["input_tokens", "cached_input_tokens", "cache_creation_input_tokens", "output_tokens", "reasoning_output_tokens", "total_tokens"];
9
+ const keyOf = (row) => row?.source === "kimi" && typeof row.model === "string" && typeof row.hour_start === "string"
10
+ ? `kimi|${row.model}|${row.hour_start}` : null;
11
+ const digest = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
12
+
13
+ async function readBytes(file) {
14
+ try { return await fs.readFile(file); } catch (error) { if (error.code === "ENOENT") return Buffer.alloc(0); throw error; }
15
+ }
16
+ async function readJson(file) {
17
+ const bytes = await readBytes(file);
18
+ return bytes.length ? JSON.parse(bytes.toString("utf8")) : null;
19
+ }
20
+ function latestRows(bytes) {
21
+ const rows = new Map();
22
+ for (const line of bytes.toString("utf8").split("\n")) {
23
+ let row;
24
+ try { row = JSON.parse(line); } catch { continue; }
25
+ const key = keyOf(row);
26
+ if (key) rows.set(key, row);
27
+ }
28
+ return rows;
29
+ }
30
+ function sumsByHour(rows) {
31
+ const totals = new Map();
32
+ for (const row of rows.values()) {
33
+ const total = totals.get(row.hour_start) || Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
34
+ for (const field of TOKEN_FIELDS) {
35
+ const count = Number(row[field] || 0);
36
+ if (!Number.isSafeInteger(count) || count < 0) throw new Error("Kimi repair encountered invalid token totals");
37
+ total[field] += count;
38
+ }
39
+ if (TOKEN_FIELDS.some((field) => total[field] !== 0)) totals.set(row.hour_start, total);
40
+ }
41
+ return totals;
42
+ }
43
+ function sameTotals(before, after) {
44
+ const left = sumsByHour(before), right = sumsByHour(after);
45
+ return left.size === right.size && [...left].every(([hour, total]) => right.has(hour)
46
+ && TOKEN_FIELDS.every((field) => total[field] === right.get(hour)[field]));
47
+ }
48
+ function sameSnapshot(left, right) {
49
+ return left && right && [...TOKEN_FIELDS, "conversation_count", "billable_total_tokens"]
50
+ .every((field) => Number(left[field] || 0) === Number(right[field] || 0));
51
+ }
52
+ async function fileSignature(file) {
53
+ const handle = await fs.open(file, "r");
54
+ try {
55
+ const stat = await handle.stat();
56
+ if (!stat.isFile()) throw new Error("Kimi repair source is not a file");
57
+ return { size: stat.size, ino: stat.ino, mtimeMs: stat.mtimeMs };
58
+ } finally { await handle.close(); }
59
+ }
60
+ async function writeJournal(file, journal) {
61
+ const temp = `${file}.${crypto.randomUUID()}.tmp`;
62
+ try {
63
+ await fs.writeFile(temp, JSON.stringify(journal), { mode: 0o600, flag: "wx" });
64
+ await fs.rename(temp, file);
65
+ } finally { await fs.rm(temp, { force: true }); }
66
+ }
67
+
68
+ // Caller holds sync.lock and commits cursors after this returns, before upload.
69
+ // Only append to queue.jsonl; the caller compacts its pending suffix with
70
+ // minPendingBytes:0 after cursor commit, preserving uploaded bytes and offset.
71
+ // Parsers are injected to keep rollout.js independent of its migration helper.
72
+ async function repairKimiModelAttribution({ cursors, queuePath, queueStatePath, legacyFiles = [], officialFiles = [], parseLegacy, parseOfficial, env = process.env }) {
73
+ const journalPath = `${queuePath}.kimi-model-repair.json`;
74
+ const migrations = cursors.migrations || {};
75
+ const applied = migrations[KIMI_MODEL_REPAIR_KEY];
76
+ if (applied?.status === "applied" || applied?.status === "noop") {
77
+ await fs.rm(journalPath, { force: true });
78
+ return { repaired: false, pendingCompaction: applied.pendingCompaction === true };
79
+ }
80
+ let journal = await readJson(journalPath);
81
+ if (!journal) {
82
+ const rawQueue = await readBytes(queuePath);
83
+ const live = latestRows(rawQueue);
84
+ for (const [key, bucket] of Object.entries(cursors.hourly?.buckets || {})) {
85
+ if (!key.startsWith("kimi|") || live.has(key)) continue;
86
+ const [, model, hour_start] = key.split("|");
87
+ live.set(key, { source: "kimi", model, hour_start, ...bucket.totals });
88
+ }
89
+ if (![...live.values()].some((row) => Number(row.total_tokens) > 0)) {
90
+ (cursors.migrations ||= {})[KIMI_MODEL_REPAIR_KEY] = { status: "noop", appliedAt: new Date().toISOString() };
91
+ return { repaired: false, pendingCompaction: false };
92
+ }
93
+ const files = [...new Set([...legacyFiles, ...officialFiles])];
94
+ const discovered = new Set(files);
95
+ const previousFiles = Object.entries(cursors.kimi?.fileOffsets || {}).concat(Object.entries(cursors.kimiCode?.fileOffsets || {}));
96
+ if (!officialFiles.length || previousFiles.some(([file]) => !discovered.has(file))) {
97
+ return { repaired: false, pendingCompaction: false, deferred: "missing-source" };
98
+ }
99
+ const signatures = new Map();
100
+ try {
101
+ for (const file of files) signatures.set(file, await fileSignature(file));
102
+ if (previousFiles.some(([file, prior]) => signatures.get(file).size < Number(prior.size || 0))) {
103
+ return { repaired: false, pendingCompaction: false, deferred: "truncated-source" };
104
+ }
105
+ } catch {
106
+ return { repaired: false, pendingCompaction: false, deferred: "unreadable-source" };
107
+ }
108
+ const scratch = await fs.mkdtemp(path.join(path.dirname(queuePath), ".kimi-model-repair-"));
109
+ const rebuilt = { version: 1, hourly: { version: 3, buckets: {}, groupQueued: {} } };
110
+ let corrected;
111
+ try {
112
+ const scratchQueue = path.join(scratch, "queue.jsonl");
113
+ await parseLegacy({ wireFiles: legacyFiles, cursors: rebuilt, queuePath: scratchQueue, env });
114
+ await parseOfficial({ wireFiles: officialFiles, cursors: rebuilt, queuePath: scratchQueue, env });
115
+ corrected = latestRows(await readBytes(scratchQueue));
116
+ for (const file of files) {
117
+ if (JSON.stringify(await fileSignature(file)) !== JSON.stringify(signatures.get(file))) {
118
+ return { repaired: false, pendingCompaction: false, deferred: "source-changed" };
119
+ }
120
+ }
121
+ } finally { await fs.rm(scratch, { recursive: true, force: true }); }
122
+ // Missing/corrupt history or newly appended usage must first settle through
123
+ // ordinary sync. A model-only repair may never change per-hour token totals.
124
+ if (!sameTotals(live, corrected)) return { repaired: false, pendingCompaction: false, deferred: "totals-not-conserved" };
125
+ const currentQueue = await readBytes(queuePath);
126
+ if (!currentQueue.equals(rawQueue)) return { repaired: false, pendingCompaction: false, deferred: "queue-changed" };
127
+ const state = await readJson(queueStatePath) || {};
128
+ const offset = Number(state.offset || 0);
129
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > rawQueue.length || (offset > 0 && rawQueue[offset - 1] !== 10)) {
130
+ return { repaired: false, pendingCompaction: false, deferred: "invalid-upload-offset" };
131
+ }
132
+ const corrections = [];
133
+ for (const [key, old] of live) {
134
+ if (!corrected.has(key)) corrections.push({ source: "kimi", model: old.model, hour_start: old.hour_start,
135
+ ...Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0])), billable_total_tokens: 0, conversation_count: 0 });
136
+ }
137
+ for (const [key, row] of corrected) if (!sameSnapshot(live.get(key), row)) corrections.push(row);
138
+ journal = { version: 1, uploadedOffset: offset, uploadedPrefixSha256: digest(rawQueue.subarray(0, offset)),
139
+ rows: corrections, kimi: rebuilt.kimi || {}, kimiCode: rebuilt.kimiCode || {}, hourly: rebuilt.hourly,
140
+ preparedAt: new Date().toISOString() };
141
+ await writeJournal(journalPath, journal);
142
+ }
143
+ const rawQueue = await readBytes(queuePath);
144
+ const state = await readJson(queueStatePath) || {};
145
+ if (journal.version !== 1 || !Number.isSafeInteger(journal.uploadedOffset) || journal.uploadedOffset < 0
146
+ || rawQueue.length < journal.uploadedOffset || Number(state.offset || 0) < journal.uploadedOffset
147
+ || digest(rawQueue.subarray(0, journal.uploadedOffset)) !== journal.uploadedPrefixSha256) {
148
+ return { repaired: false, pendingCompaction: false, deferred: "uploaded-prefix-changed" };
149
+ }
150
+ if (journal.rows.length) {
151
+ // A prior append can have stopped halfway through a JSON line. Preserve
152
+ // those bytes, terminate that incomplete record, then append complete LWW
153
+ // snapshots again. The journal bypasses conservation checks on partial work.
154
+ const separator = rawQueue.length && rawQueue.at(-1) !== 10 ? "\n" : "";
155
+ await fs.appendFile(queuePath, separator + journal.rows.map(JSON.stringify).join("\n") + "\n");
156
+ }
157
+ const hourly = (cursors.hourly ||= { version: 3, buckets: {}, groupQueued: {} });
158
+ for (const field of ["buckets", "groupQueued"]) {
159
+ hourly[field] ||= {};
160
+ for (const key of Object.keys(hourly[field])) if (key.startsWith("kimi|")) delete hourly[field][key];
161
+ for (const [key, value] of Object.entries(journal.hourly?.[field] || {})) if (key.startsWith("kimi|")) hourly[field][key] = value;
162
+ }
163
+ cursors.kimi = journal.kimi;
164
+ cursors.kimiCode = journal.kimiCode;
165
+ (cursors.migrations ||= {})[KIMI_MODEL_REPAIR_KEY] = {
166
+ status: "applied", appliedAt: new Date().toISOString(), correctedBuckets: journal.rows.length,
167
+ pendingCompaction: journal.rows.length > 0,
168
+ };
169
+ // Leave the journal until the next call observes the persisted marker.
170
+ return { repaired: true, pendingCompaction: journal.rows.length > 0 };
171
+ }
172
+
173
+ module.exports = { KIMI_MODEL_REPAIR_KEY, repairKimiModelAttribution };
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const { physicalJsonlRecords } = require("./jsonl-lines");
7
+ const { resolveKimiDefaultModel } = require("./rollout");
8
+ const { mergePerformance } = require("./session-performance");
9
+
10
+ function readObject(file) {
11
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return {}; }
12
+ }
13
+
14
+ function kimiSessionLocation(file) {
15
+ const official = path.basename(path.dirname(path.dirname(file))) === "agents";
16
+ const directory = official ? path.dirname(path.dirname(path.dirname(file))) : path.dirname(file);
17
+ const id = path.basename(directory);
18
+ const imported = official ? readObject(path.join(directory, "state.json"))?.custom : null;
19
+ // The official migrator keeps the legacy UUID under ses_<UUID>. Group its
20
+ // message-only mirror with the original wire, retaining any new usage tail.
21
+ const identity = imported?.imported_from_kimi_cli && typeof imported.kimi_cli_session_id === "string"
22
+ ? imported.kimi_cli_session_id : id.replace(/^(?:ses_|session_)(?=[0-9a-f-]{36}$)/i, "");
23
+ return { directory, id, identity, official, main: !official || path.basename(path.dirname(file)) === "main" };
24
+ }
25
+
26
+ function kimiSessionDependencies(files) {
27
+ return [...new Set(files.flatMap((file) => {
28
+ const location = kimiSessionLocation(file);
29
+ const root = path.dirname(path.dirname(path.dirname(location.directory)));
30
+ return [path.join(location.directory, "state.json"), path.join(root, "config.toml"), path.join(root, "kimi.json")];
31
+ }))];
32
+ }
33
+
34
+ function discoverKimiWireFiles(roots, incomplete) {
35
+ const files = new Set();
36
+ function walk(directory, depth = 0) {
37
+ if (depth > 5) return;
38
+ let entries;
39
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch (error) {
40
+ if (error.code !== "ENOENT") incomplete.push({ path: directory, error: error.code || "unreadable" });
41
+ return;
42
+ }
43
+ for (const entry of entries) {
44
+ if (entry.name.startsWith(".")) continue;
45
+ const file = path.join(directory, entry.name);
46
+ if (entry.isDirectory()) walk(file, depth + 1);
47
+ else if (entry.isFile() && entry.name === "wire.jsonl") files.add(file);
48
+ }
49
+ }
50
+ for (const root of roots) walk(path.join(root, "sessions"));
51
+ return [...files].sort();
52
+ }
53
+
54
+ const count = (value) => Number.isFinite(Number(value)) ? Math.max(0, Math.floor(Number(value))) : 0;
55
+ function kimiUsage(usage, legacy) {
56
+ if (!usage || typeof usage !== "object") return null;
57
+ let input, cached, created, output;
58
+ // Same column semantics as parseKimiIncremental / parseKimiCodeIncremental.
59
+ if (legacy || usage.inputOther != null) {
60
+ input = count(legacy ? usage.input_other : usage.inputOther);
61
+ cached = count(legacy ? usage.input_cache_read : usage.inputCacheRead);
62
+ created = count(legacy ? usage.input_cache_creation : usage.inputCacheCreation);
63
+ output = count(usage.output);
64
+ } else {
65
+ created = count(usage.cache_creation_input_tokens);
66
+ cached = count(usage.cache_read_input_tokens ?? usage.input_tokens_details?.cached_tokens);
67
+ input = Math.max(0, count(usage.input_tokens) - (usage.cache_read_input_tokens != null ? 0 : cached));
68
+ output = count(usage.output_tokens);
69
+ }
70
+ return { input_tokens: input, cached_input_tokens: cached, cache_creation_input_tokens: created,
71
+ output_tokens: output, reasoning_output_tokens: 0, total_tokens: input + cached + created + output };
72
+ }
73
+
74
+ const modelAlias = (value) => typeof value === "string" && value.trim() ? value.trim().split("/").pop() : null;
75
+ const EDIT_TOOLS = new Set(["edit", "write", "multiedit", "notebookedit", "strreplacefile", "writefile", "apply_patch"]);
76
+ const AGENT_TOOLS = new Set(["agent", "agentswarm", "task"]);
77
+ const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
78
+
79
+ // Reads both wire protocols into metadata only. A full inventory shares its
80
+ // usage-ID set, as sync does, so copied/forked step records count only once.
81
+ async function readKimiSessions(files) {
82
+ const groups = new Map();
83
+ for (const file of [...new Set(files)]) {
84
+ const location = kimiSessionLocation(file);
85
+ const group = groups.get(location.identity) || { identity: location.identity, files: [], createdAt: Infinity };
86
+ const state = location.official ? readObject(path.join(location.directory, "state.json")) : {};
87
+ group.createdAt = Math.min(group.createdAt, Number(state.createdAt) || Infinity);
88
+ group.files.push({ file, ...location, state });
89
+ groups.set(location.identity, group);
90
+ }
91
+ const seenUsage = new Set();
92
+ const seenSignals = new Set();
93
+ const rows = [];
94
+ for (const group of [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a.identity.localeCompare(b.identity))) {
95
+ const tokens = kimiUsage({}, true);
96
+ const modelUsage = new Map();
97
+ const buckets = new Map();
98
+ const times = new Set();
99
+ let turns = 0, edits = 0, retries = 0, agents = 0, errors = 0;
100
+ let lastPrompt = null;
101
+ let cwd = null;
102
+ let resumeId = null;
103
+ const modelsForEdits = new Map();
104
+ // Legacy first preserves migrated user turns; context.append_message in
105
+ // the imported wire is history, never another prompt or usage event.
106
+ group.files.sort((a, b) => Number(a.official) - Number(b.official) || Number(b.main) - Number(a.main) || a.file.localeCompare(b.file));
107
+ for (const location of group.files) {
108
+ const root = path.dirname(path.dirname(path.dirname(location.directory)));
109
+ let model = resolveKimiDefaultModel({ KIMI_HOME: root });
110
+ if (location.official && location.main) {
111
+ resumeId = location.id;
112
+ if (typeof location.state.workDir === "string") cwd = location.state.workDir;
113
+ }
114
+ if (!location.official && !cwd) {
115
+ const workspaceHash = path.basename(path.dirname(location.directory));
116
+ const workDirs = readObject(path.join(root, "kimi.json"))?.work_dirs;
117
+ const workspace = Array.isArray(workDirs) && workDirs.find((row) => typeof row?.path === "string"
118
+ && crypto.createHash("md5").update(row.path).digest("hex") === workspaceHash);
119
+ if (workspace) cwd = workspace.path;
120
+ }
121
+ let turnOpen = false, hadEdit = false, turnModel = model;
122
+ const closeTurn = () => {
123
+ if (hadEdit) { edits++; modelsForEdits.set(turnModel, (modelsForEdits.get(turnModel) || 0) + 1); }
124
+ hadEdit = false; turnOpen = false;
125
+ };
126
+ const input = fs.createReadStream(location.file);
127
+ for await (const { line, terminated } of physicalJsonlRecords(input)) {
128
+ if (!terminated || !line) continue;
129
+ let entry;
130
+ try { entry = JSON.parse(line); } catch { continue; }
131
+ if (!entry || typeof entry !== "object") continue;
132
+ const message = entry.message && typeof entry.message === "object" ? entry.message : null;
133
+ const event = entry.type === "context.append_loop_event" ? entry.event : entry;
134
+ const timestampMs = Number(location.official ? entry.time ?? event?.time : entry.timestamp ?? message?.payload?.timestamp) * (location.official ? 1 : 1000);
135
+ const timestamp = Number.isFinite(timestampMs) && timestampMs > 0 && timestampMs <= 8640000000000000
136
+ ? new Date(timestampMs).toISOString() : null;
137
+ if (timestamp) times.add(timestampMs);
138
+ if (["config.update", "profile.bind", "llm.request"].includes(entry.type)) {
139
+ model = modelAlias(entry.modelAlias || entry.model) || model;
140
+ if (location.main && typeof (entry.cwd || entry.environmentDisclosure?.cwd) === "string") cwd = entry.cwd || entry.environmentDisclosure.cwd;
141
+ }
142
+ const isLegacyUsage = message?.type === "StatusUpdate";
143
+ const usage = isLegacyUsage ? message.payload?.token_usage : event?.type === "step.end" ? event.usage : null;
144
+ const usageId = isLegacyUsage ? message.payload?.message_id : event?.uuid;
145
+ if (usage && typeof usageId === "string" && timestamp) {
146
+ const ids = [isLegacyUsage ? `message:${usageId}` : `step:${usageId}`];
147
+ if (!isLegacyUsage && typeof event.messageId === "string") ids.push(`message:${event.messageId}`);
148
+ const delta = kimiUsage(usage, isLegacyUsage);
149
+ if (delta.total_tokens > 0 && !ids.some((id) => seenUsage.has(id))) {
150
+ for (const id of ids) seenUsage.add(id);
151
+ const modelRow = modelUsage.get(model) || { model, ...kimiUsage({}, true), usage_events: 0 };
152
+ const bucketTime = new Date(Math.floor(timestampMs / 60000) * 60000).toISOString();
153
+ const key = `${model}\0${bucketTime}`;
154
+ const bucket = buckets.get(key) || { model, timestamp: bucketTime, ...kimiUsage({}, true), usage_events: 0 };
155
+ for (const field of Object.keys(delta)) { tokens[field] += delta[field]; modelRow[field] += delta[field]; bucket[field] += delta[field]; }
156
+ modelRow.usage_events++; bucket.usage_events++;
157
+ const streamMs = count(event.llmStreamDurationMs);
158
+ const firstMs = Number(event.llmFirstTokenLatencyMs);
159
+ const performance = {
160
+ ...(delta.output_tokens >= 200 && streamMs >= 1000 && streamMs <= 3600000
161
+ ? { estimated_output_tokens: delta.output_tokens, estimated_duration_ms: streamMs, estimated_request_count: 1 } : {}),
162
+ ...(event.llmFirstTokenLatencyMs != null && Number.isFinite(firstMs) && firstMs >= 0
163
+ ? { first_response_total_ms: firstMs, first_response_sample_count: 1 } : {}),
164
+ };
165
+ modelRow.performance = mergePerformance(modelRow.performance, performance);
166
+ modelUsage.set(model, modelRow); buckets.set(key, bucket);
167
+ turnModel = model;
168
+ }
169
+ }
170
+ if (!location.main) continue;
171
+ const signalId = hash(line);
172
+ if (seenSignals.has(signalId)) continue;
173
+ seenSignals.add(signalId);
174
+ const prompt = message?.type === "TurnBegin" ? message.payload?.user_input
175
+ : entry.type === "turn.prompt" && (!entry.origin?.kind || entry.origin.kind === "user") ? entry.input : null;
176
+ if (prompt != null) {
177
+ closeTurn(); turns++; turnOpen = true; turnModel = model;
178
+ // Fingerprints live only for this scan; no text or hash is persisted.
179
+ const fingerprint = hash(JSON.stringify(prompt));
180
+ if (lastPrompt && fingerprint === lastPrompt) retries++;
181
+ lastPrompt = fingerprint;
182
+ }
183
+ const tool = message?.type === "ToolCall" ? message.payload?.function?.name : event?.type === "tool.call" ? event.name : null;
184
+ if (typeof tool === "string") {
185
+ const name = tool.toLowerCase();
186
+ if (turnOpen && EDIT_TOOLS.has(name)) hadEdit = true;
187
+ if (AGENT_TOOLS.has(name)) agents++;
188
+ }
189
+ if (entry.type === "turn.step.retrying" || message?.type === "StepInterrupted") errors++;
190
+ if (message?.type === "TurnEnd" || entry.type === "turn.ended") closeTurn();
191
+ }
192
+ closeTurn();
193
+ }
194
+ const orderedTimes = [...times].sort((a, b) => a - b);
195
+ let activeMs = 0;
196
+ for (let i = 1; i < orderedTimes.length; i++) {
197
+ const gap = orderedTimes[i] - orderedTimes[i - 1];
198
+ if (gap <= 30 * 60 * 1000) activeMs += gap;
199
+ }
200
+ const models = [...modelUsage.values()].map((row) => ({ ...row, edit_turns: modelsForEdits.get(row.model) || 0 }));
201
+ if (!tokens.total_tokens && !turns) continue;
202
+ rows.push({ identity: group.identity, files: group.files.map((row) => row.file), session_id: resumeId,
203
+ title: null, project_ref: cwd, model: models.length === 1 ? models[0].model : "unknown", model_usage: models,
204
+ usage_buckets: [...buckets.values()], tokens, turns, edit_turns: edits, retry_turns: retries,
205
+ subagent_calls: agents, subagent_types: {}, error_count: errors,
206
+ usage_events: models.reduce((sum, row) => sum + row.usage_events, 0),
207
+ started_at: orderedTimes.length ? new Date(orderedTimes[0]).toISOString() : null,
208
+ ended_at: orderedTimes.length ? new Date(orderedTimes.at(-1)).toISOString() : null, active_ms: activeMs,
209
+ performance: mergePerformance(...models.map((row) => row.performance)),
210
+ provenance: { source: "local-session-log", confidence: "observed", retry_confidence: "inferred", content_retained: false, usage: "kimi-wire-metadata" },
211
+ });
212
+ }
213
+ return rows;
214
+ }
215
+
216
+ module.exports = { discoverKimiWireFiles, kimiSessionDependencies, readKimiSessions };
@@ -13,6 +13,7 @@ const {
13
13
  normalizeUsageScope,
14
14
  } = require("./source-metadata");
15
15
  const { accountSlugFor, fetchAccountUsage, mintAccessToken, invalidateCloudAccountPayloadCache, getCloudAccountSessionGeneration } = require("./cloud-account");
16
+ const { resetCloudDeviceUsage, readCloudUsageReset } = require("./cloud-usage-reset");
16
17
  const { getOrCreateMachineId, computeStableMachineId } = require("./machine-id");
17
18
  const { functionUrlFor, fetchFunctionResponse } = require("./function-url");
18
19
  const { createCloudDeviceTokenStore } = require("./cloud-device-token");
@@ -2162,6 +2163,39 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2162
2163
  return true;
2163
2164
  }
2164
2165
 
2166
+ if (p === "/functions/tokentracker-cloud-usage-reset") {
2167
+ if (!isAuthorizedLocalMutation(req) || req.headers["sec-fetch-site"] === "cross-site") { json(res, { error: "Unauthorized" }, 401); return true; }
2168
+ try {
2169
+ if (String(req.method).toUpperCase() === "GET") {
2170
+ json(res, { pending: Boolean(readCloudUsageReset(trackerDataDir)) }); return true;
2171
+ }
2172
+ if (String(req.method).toUpperCase() !== "POST") { json(res, { error: "Method Not Allowed" }, 405); return true; }
2173
+ const body = await readJsonBody(req);
2174
+ const session = localCloudSession;
2175
+ if (!getCloudSyncPref() || !session || body.cloudSessionId !== session.id || body.expectedOwnerId !== session.ownerId) {
2176
+ json(res, { code: "auth_session_changed" }, 409); return true;
2177
+ }
2178
+ const metadata = localSyncDeviceMetadata.get(session.token);
2179
+ const accessToken = String(req.headers.authorization || "").replace(/^Bearer /i, "").trim();
2180
+ const runtime = assertInsforgeRuntime(getRuntime());
2181
+ let claims;
2182
+ try { claims = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString("utf8")); } catch { /* rejected below */ }
2183
+ if (claims?.sub !== session.ownerId || !Number.isFinite(claims?.exp) || claims.exp * 1000 <= Date.now()) {
2184
+ json(res, { code: "auth_required" }, 401); return true;
2185
+ }
2186
+ const isCurrent = () => getCloudSyncPref() && localCloudSession === session && session.generation === localCloudGeneration &&
2187
+ claims.exp * 1000 > Date.now() && getRuntime().baseUrl === session.baseUrl;
2188
+ const result = await resetCloudDeviceUsage({ trackerDir: trackerDataDir, ownerId: session.ownerId,
2189
+ deviceId: metadata?.deviceId, baseUrl: session.baseUrl, anonKey: runtime.anonKey, accessToken, isCurrent });
2190
+ if (!isCurrent()) { json(res, { code: "auth_session_changed" }, 409); return true; }
2191
+ clearLocalSyncDeviceTokenCache();
2192
+ invalidateAfterAccountUpload();
2193
+ invalidateCloudAccountPayloadCache();
2194
+ json(res, { ...result, pending: true });
2195
+ } catch (error) { json(res, { error: error.code || "cloud_usage_reset_failed", code: error.code || null }, error.status || 502); }
2196
+ return true;
2197
+ }
2198
+
2165
2199
  // --- local-sync (POST) ---
2166
2200
  if (p === "/functions/tokentracker-local-sync") {
2167
2201
  if (String(req.method || "GET").toUpperCase() !== "POST") {
@@ -2263,10 +2297,20 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2263
2297
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) {
2264
2298
  json(res, { ok: false, code: "auth_session_changed" }, 409); return true;
2265
2299
  }
2300
+ if (extraEnv.TOKENTRACKER_DEVICE_TOKEN) {
2301
+ const metadata = localSyncDeviceMetadata.get(extraEnv.TOKENTRACKER_DEVICE_TOKEN);
2302
+ if (metadata?.deviceId) extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID = metadata.deviceId;
2303
+ }
2266
2304
  await Promise.all([...activeCloudSyncs].filter(controller => controller.signal.aborted).map(controller => controller.done).filter(Boolean));
2267
2305
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
2268
2306
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && localSyncDeviceTokenUserId) {
2269
- const binding = JSON.stringify({ ownerId: localSyncDeviceTokenUserId, baseUrl: localSyncBaseUrl || getRuntime().baseUrl });
2307
+ const uploadBaseUrl = localSyncBaseUrl || getRuntime().baseUrl;
2308
+ const reset = readCloudUsageReset(trackerDataDir);
2309
+ if (reset && (reset.ownerId !== localSyncDeviceTokenUserId || reset.baseUrl.replace(/\/$/, "") !== uploadBaseUrl.replace(/\/$/, "") ||
2310
+ (extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID && reset.deviceId !== extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID))) {
2311
+ json(res, { ok: false, code: "cloud_usage_reset_owner_changed" }, 409); return true;
2312
+ }
2313
+ const binding = JSON.stringify({ ownerId: localSyncDeviceTokenUserId, baseUrl: uploadBaseUrl });
2270
2314
  const ownerPath = path.join(path.dirname(qp), "cloud-upload-owner.json");
2271
2315
  let previous = "";
2272
2316
  try { previous = fs.readFileSync(ownerPath, "utf8"); } catch { /* an unbound legacy cursor cannot prove its previous owner */ }
@@ -2279,6 +2323,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2279
2323
  const controller = new AbortController();
2280
2324
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN) activeCloudSyncs.add(controller);
2281
2325
  const syncStartedAt = Date.now();
2326
+ if (localSyncDeviceTokenUserId) extraEnv.TOKENTRACKER_CLOUD_OWNER_ID = localSyncDeviceTokenUserId;
2282
2327
  extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID = crypto.randomUUID();
2283
2328
  const syncPromise = runSyncCommand(extraEnv, {
2284
2329
  signal: controller.signal,
@@ -250,7 +250,8 @@ function computeRowCost(row) {
250
250
  row.source === "acode" ||
251
251
  row.source === "every-code" ||
252
252
  row.source === "omo" ||
253
- row.source === "cline";
253
+ row.source === "cline" ||
254
+ row.source === "muse";
254
255
  const reasoningCost = reasoningIncludedInOutput
255
256
  ? 0
256
257
  : (row.reasoning_output_tokens || 0) * (pricing.output || 0);