tokentracker-cli 1.4.9 → 1.4.11

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 (128) hide show
  1. package/README.de.md +3 -3
  2. package/README.ja.md +3 -3
  3. package/README.ko.md +3 -3
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +3 -3
  6. package/dashboard/dist/assets/{AchievementBadge-_dRLgxnp.js → AchievementBadge-DALWiLQQ.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-DtvlZLlv.js → AchievementsPage-CjWobmn1.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-OgzKkbes.js → AchievementsSection-CTryGWII.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D4Rlm_um.js → BadgeDetailModal-D4lMxewA.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-errJ5IvB.js → BotAnimated-CsLNf9WH.js} +1 -1
  11. package/dashboard/dist/assets/{Card-esELVQcD.js → Card-BOnNTrmF.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-3sH4Dbx9.js → CloudCheckoutPage-CrY15xqT.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-Bi_vgiqE.js → CloudPage-hNSCMzVG.js} +2 -2
  14. package/dashboard/dist/assets/{CommandPalette-I0aze2EJ.js → CommandPalette-CE4UfPXk.js} +1 -1
  15. package/dashboard/dist/assets/{CommunityStatsModal-CEn2g4J9.js → CommunityStatsModal-CLUegC6z.js} +1 -1
  16. package/dashboard/dist/assets/{ConfirmModal-D7SfFCaf.js → ConfirmModal-D3wwfiYB.js} +1 -1
  17. package/dashboard/dist/assets/{Controls-Dyb5Vmk_.js → Controls-B_ta_8zw.js} +1 -1
  18. package/dashboard/dist/assets/{DashboardPage-CMHX8zDq.js → DashboardPage-RevVPZFt.js} +1 -1
  19. package/dashboard/dist/assets/{DateRangePopover-CgmxsHVR.js → DateRangePopover-CYud1FKu.js} +1 -1
  20. package/dashboard/dist/assets/DevicePage-D_eSLfM3.js +1 -0
  21. package/dashboard/dist/assets/{DialogClose-DzR109OA.js → DialogClose-Bu1ct4DA.js} +1 -1
  22. package/dashboard/dist/assets/DialogDescription-GUHQVNGf.js +1 -0
  23. package/dashboard/dist/assets/{DialogTitle-BLH1MgOX.js → DialogTitle-BJxM52J1.js} +1 -1
  24. package/dashboard/dist/assets/{ElectricLogo-DcpBcJIy.js → ElectricLogo-DYVZebgD.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-CBAXe_zb.js → FadeIn-CVaq5ALh.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-BZjQ3uJo.js → HeaderGithubStar-BCwYSguq.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-Bq4HuWOZ.js → HoverTooltip-YsG-TdOX.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-Oiah1-YB.js → IpCheckPage-C0qiyZXr.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-Cc1FfkWg.js → LandingPage-BlRJWwvx.js} +2 -2
  30. package/dashboard/dist/assets/{LeaderboardPage-Bc2j_DQY.js → LeaderboardPage-DcCCKL2q.js} +2 -2
  31. package/dashboard/dist/assets/{LeaderboardProBadge-w1olVHVp.js → LeaderboardProBadge-I_oW0KCc.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-mgG7mdl8.js → LeaderboardProfileModal-DM5Vc7MB.js} +3 -3
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-DqxyooT5.js → LeaderboardProfilePage-DnYBBGNe.js} +1 -1
  34. package/dashboard/dist/assets/LimitsPage-Cyy3PI8p.js +6 -0
  35. package/dashboard/dist/assets/{LocalOnlyNotice-DZLgI75v.js → LocalOnlyNotice-083BoEX9.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-DVNU_QtS.js → LoginCard-CN1db2Rj.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-yxi5pCwC.js → LoginModal-Dfi7_ory.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-CW9pu4mf.js → LoginPage-BzMdiYlx.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-DwZsq4nC.js → PetPage-CsE95lfJ.js} +2 -2
  40. package/dashboard/dist/assets/{PopoverPopup-DYGmmoD8.js → PopoverPopup-U7DTTTE0.js} +1 -1
  41. package/dashboard/dist/assets/{ProviderIcon-BKfj89GN.js → ProviderIcon-CnLfT5d9.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-gQdgCp9s.js → ResetPasswordPage-De2RzCj-.js} +1 -1
  43. package/dashboard/dist/assets/{Select-DvSR56Lj.js → Select-BS5MWHdB.js} +1 -1
  44. package/dashboard/dist/assets/{SelectItemText-DXo30s3O.js → SelectItemText-D3i379K-.js} +1 -1
  45. package/dashboard/dist/assets/{SelfHostPage-jA51Nckw.js → SelfHostPage-CTL0dnp5.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-DEkRRPx8.js → ServiceStatusPage-BmEvOk3B.js} +1 -1
  47. package/dashboard/dist/assets/{SessionsPage-J7hvGPCj.js → SessionsPage-DIog3JA_.js} +2 -2
  48. package/dashboard/dist/assets/{SettingsPage-ewH1Mzsm.js → SettingsPage-Cgpiwh-C.js} +3 -3
  49. package/dashboard/dist/assets/{SkillsPage-BaeBHhU7.js → SkillsPage-DpagshiB.js} +1 -1
  50. package/dashboard/dist/assets/{TokenGalaxy-D52pHpzj.js → TokenGalaxy-Do1U_-E3.js} +1 -1
  51. package/dashboard/dist/assets/{TooltipPopup-mLpESQcv.js → TooltipPopup-MOAnEqf3.js} +1 -1
  52. package/dashboard/dist/assets/{TrendMonitor-ErowR5GE.js → TrendMonitor-B55RUbQm.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-Bf1uuKE7.js → WidgetsPage-CehbGk-6.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-C8qacvpR.js → WrappedPage-Cqt-U0Zx.js} +1 -1
  55. package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
  56. package/dashboard/dist/assets/{__vite-browser-external-CIpYjfAT.js → __vite-browser-external-CR0vo15z.js} +3 -3
  57. package/dashboard/dist/assets/{agent-logos-tmdKvCcK.js → agent-logos-DuW-DWAl.js} +1 -1
  58. package/dashboard/dist/assets/{arrow-left-CRKi420c.js → arrow-left-DIvQjuji.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-right-C_o_iQbK.js → arrow-right-BURLycHb.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-up-right-N5YqKKnI.js → arrow-up-right-Bl1QMMkY.js} +1 -1
  61. package/dashboard/dist/assets/{calendar-days-BJh4FDj9.js → calendar-days-CmIWJC3A.js} +1 -1
  62. package/dashboard/dist/assets/{check-BfHGb92g.js → check-DB2ZTWor.js} +1 -1
  63. package/dashboard/dist/assets/{chevron-down-D1pO8Xgc.js → chevron-down-BflN3NJt.js} +1 -1
  64. package/dashboard/dist/assets/{cloud-checkout-Ds13TmYz.js → cloud-checkout-BugQolSC.js} +1 -1
  65. package/dashboard/dist/assets/{composite-DDEsQKPD.js → composite-DJsqXsjA.js} +1 -1
  66. package/dashboard/dist/assets/{copy-CJTosYLe.js → copy-CtB-GPN2.js} +1 -1
  67. package/dashboard/dist/assets/{download-Bkqx3c3y.js → download-x5Botpqc.js} +1 -1
  68. package/dashboard/dist/assets/{external-link-IYUo5LcK.js → external-link-DH9sRkJW.js} +1 -1
  69. package/dashboard/dist/assets/{flame-BD_hiMfM.js → flame-DEcsokoL.js} +1 -1
  70. package/dashboard/dist/assets/{format-tokens-DxwbhPag.js → format-tokens-BH0jc1MG.js} +1 -1
  71. package/dashboard/dist/assets/{gift-DLzFdL-1.js → gift-BZk9muk9.js} +1 -1
  72. package/dashboard/dist/assets/{icons-CbrWeFPS.js → icons-BMf9-bfS.js} +1 -1
  73. package/dashboard/dist/assets/{index-Dj1QnoPF.js → index-BEtrik3j.js} +1 -1
  74. package/dashboard/dist/assets/{index-DfzCE-FL.js → index-Cm-9apOR.js} +1 -1
  75. package/dashboard/dist/assets/{index-AVEWo-aY.js → index-TErx-xPb.js} +1 -1
  76. package/dashboard/dist/assets/{index-3xGpnZnP.js → index-axJYocSc.js} +1 -1
  77. package/dashboard/dist/assets/{info-odlIlCnm.js → info-DWdPAxu8.js} +1 -1
  78. package/dashboard/dist/assets/{link-2-Bt28Ey9r.js → link-2-c3ZZd-w-.js} +1 -1
  79. package/dashboard/dist/assets/{loader-circle-lGnIhIFS.js → loader-circle-BKdtxfky.js} +1 -1
  80. package/dashboard/dist/assets/{local-usage-transfer-DSS0WaL-.js → local-usage-transfer-BFTJyQl3.js} +1 -1
  81. package/dashboard/dist/assets/{modifiers.esm-CitRa_Z1.js → modifiers.esm-C3yIfOiP.js} +1 -1
  82. package/dashboard/dist/assets/{monitor-smartphone-1T0GFcOz.js → monitor-smartphone-DbdL7Mei.js} +1 -1
  83. package/dashboard/dist/assets/{play-d6YYOmED.js → play-qtWt2ArI.js} +1 -1
  84. package/dashboard/dist/assets/{plus-GrGV9Fkm.js → plus-C_-__5vi.js} +1 -1
  85. package/dashboard/dist/assets/{provider-display-D1v3cgDm.js → provider-display-IKHnhvaj.js} +1 -1
  86. package/dashboard/dist/assets/{react-DYCAyQbo.js → react-H6DfeMUm.js} +1 -1
  87. package/dashboard/dist/assets/{safePolygon-iPu060Qe.js → safePolygon-OEQJQTVa.js} +1 -1
  88. package/dashboard/dist/assets/{search-AQhOTwzm.js → search-MZE76qNU.js} +1 -1
  89. package/dashboard/dist/assets/{server-uYTbWPMd.js → server-CUWYLMjG.js} +1 -1
  90. package/dashboard/dist/assets/{shield-check-DB1NVCDf.js → shield-check-lA0Ja0RB.js} +1 -1
  91. package/dashboard/dist/assets/{skills-api-D7X10n0t.js → skills-api-CJJIPvSe.js} +1 -1
  92. package/dashboard/dist/assets/{sortable.esm-B13KhrCd.js → sortable.esm-DTnvt1IN.js} +1 -1
  93. package/dashboard/dist/assets/{store-D6XHiq8o.js → store-CC0YKrxA.js} +1 -1
  94. package/dashboard/dist/assets/{terminal-t37F8qyF.js → terminal-yc2l_-3m.js} +1 -1
  95. package/dashboard/dist/assets/{trash-CDHRwGsy.js → trash-Dl9f-o2h.js} +1 -1
  96. package/dashboard/dist/assets/{use-community-stats--kaGg0EW.js → use-community-stats-CZCKWCO7.js} +1 -1
  97. package/dashboard/dist/assets/{use-latest-request-guard-ChyODWU3.js → use-latest-request-guard-DTrOu0gD.js} +1 -1
  98. package/dashboard/dist/assets/use-limits-display-prefs-DGqP231e.js +1 -0
  99. package/dashboard/dist/assets/{use-native-settings-i3O4GTY1.js → use-native-settings-BEATnxB6.js} +1 -1
  100. package/dashboard/dist/assets/{use-ordered-list-BtewEEX_.js → use-ordered-list-C_HTFP5C.js} +1 -1
  101. package/dashboard/dist/assets/use-reduced-motion-CfCWjE-a.js +1 -0
  102. package/dashboard/dist/assets/{use-transform-NBAyuphu.js → use-transform-DM0QGq_O.js} +1 -1
  103. package/dashboard/dist/assets/use-usage-limits-BnjBG2fM.js +1 -0
  104. package/dashboard/dist/assets/{useCurrency-BdsxkAID.js → useCurrency-xQrRFokb.js} +1 -1
  105. package/dashboard/dist/assets/{usePositioner-BAN79miB.js → usePositioner-DkVdsypM.js} +1 -1
  106. package/dashboard/dist/assets/{useTokenFormat-CGEO_mX1.js → useTokenFormat-CvA_kIPe.js} +1 -1
  107. package/dashboard/dist/assets/{zap-BDHWp-gL.js → zap-BJNS4i_u.js} +1 -1
  108. package/dashboard/dist/index.html +5 -5
  109. package/dashboard/dist/ip-check.html +2 -2
  110. package/dashboard/dist/leaderboard.html +2 -2
  111. package/dashboard/dist/share.html +2 -2
  112. package/package.json +2 -2
  113. package/src/lib/grok-limits.js +1 -0
  114. package/src/lib/omp-session.js +480 -0
  115. package/src/lib/pricing/curated-overrides.json +35 -0
  116. package/src/lib/pricing/matcher.js +44 -15
  117. package/src/lib/pricing/seed-snapshot.json +1 -1
  118. package/src/lib/session-analytics.js +54 -8
  119. package/src/lib/usage-limits.js +20 -8
  120. package/src/lib/workbuddy-ipc.js +191 -0
  121. package/src/lib/workbuddy-limits.js +564 -0
  122. package/dashboard/dist/assets/DevicePage-9R2MfK2Z.js +0 -1
  123. package/dashboard/dist/assets/DialogDescription-CdNjUDLZ.js +0 -1
  124. package/dashboard/dist/assets/LimitsPage-BIvK0GFj.js +0 -6
  125. package/dashboard/dist/assets/__vite-browser-external-CnVnsbqV.css +0 -1
  126. package/dashboard/dist/assets/use-limits-display-prefs-katFAxOA.js +0 -1
  127. package/dashboard/dist/assets/use-reduced-motion-DPacKl9n.js +0 -1
  128. package/dashboard/dist/assets/use-usage-limits-hsv-93TF.js +0 -1
@@ -0,0 +1,480 @@
1
+ "use strict";
2
+
3
+ // Metadata-only oh-my-pi (omp) session reader for the Sessions sidecar.
4
+ // Reuses the same on-disk layout and assistant-usage contract as the hourly
5
+ // OMP sync parser in rollout.js. Never persists prompts, assistant bodies,
6
+ // tool arguments, or command output.
7
+
8
+ const fs = require("node:fs");
9
+ const fsp = require("node:fs/promises");
10
+ const path = require("node:path");
11
+ const crypto = require("node:crypto");
12
+ const { physicalJsonlRecords } = require("./jsonl-lines");
13
+ const { resolveOmpAgentDir, resolveOmpSubagentFiles } = require("./rollout");
14
+
15
+ // Same ceiling as the hourly OMP parser: tool results can be enormous and must
16
+ // not be buffered/decoded when they cannot carry usage.
17
+ const OMP_MAX_RECORD_BYTES = 64 * 1024 * 1024;
18
+
19
+ const EDIT_TOOLS = new Set([
20
+ "apply_patch",
21
+ "edit",
22
+ "write",
23
+ "multiedit",
24
+ "notebookedit",
25
+ "search_replace",
26
+ "str_replace",
27
+ "create_file",
28
+ "write_file",
29
+ "strreplace",
30
+ "writefile",
31
+ ]);
32
+ const AGENT_TOOLS = new Set(["task", "agent", "subagent", "spawn_agent", "spawn_subagent"]);
33
+
34
+ function count(value) {
35
+ const number = Number(value);
36
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
37
+ }
38
+
39
+ function emptyUsage() {
40
+ return {
41
+ input_tokens: 0,
42
+ cached_input_tokens: 0,
43
+ cache_creation_input_tokens: 0,
44
+ output_tokens: 0,
45
+ reasoning_output_tokens: 0,
46
+ total_tokens: 0,
47
+ };
48
+ }
49
+
50
+ function ompUsage(usage) {
51
+ if (!usage || typeof usage !== "object") return null;
52
+ const input = count(usage.input);
53
+ const output = count(usage.output);
54
+ const cached = count(usage.cacheRead);
55
+ const created = count(usage.cacheWrite);
56
+ const reasoning = count(usage.reasoningTokens ?? usage.reasoning);
57
+ if (input === 0 && output === 0 && cached === 0 && created === 0 && reasoning === 0) return null;
58
+ const reported = count(usage.totalTokens);
59
+ return {
60
+ input_tokens: input,
61
+ cached_input_tokens: cached,
62
+ cache_creation_input_tokens: created,
63
+ output_tokens: output,
64
+ reasoning_output_tokens: reasoning,
65
+ total_tokens: reported > 0 ? reported : input + output + cached + created + reasoning,
66
+ };
67
+ }
68
+
69
+ function addUsage(target, delta) {
70
+ for (const key of Object.keys(delta)) target[key] = count(target[key]) + count(delta[key]);
71
+ }
72
+
73
+ function modelAlias(value) {
74
+ if (typeof value !== "string") return null;
75
+ const model = value.trim();
76
+ return model || null;
77
+ }
78
+
79
+ // Match hourly ompEntryTimestampMs: reject placeholder epoch values that
80
+ // used to land usage in 1970 and vanish from current date filters.
81
+ const MIN_OMP_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
82
+
83
+ function entryTimestampMs(entry, message) {
84
+ const direct = Number(message?.timestamp);
85
+ if (Number.isFinite(direct) && direct >= MIN_OMP_TIMESTAMP_MS) return direct;
86
+ if (typeof entry?.timestamp === "string" && entry.timestamp) {
87
+ const parsed = Date.parse(entry.timestamp);
88
+ if (Number.isFinite(parsed) && parsed >= MIN_OMP_TIMESTAMP_MS) return parsed;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function sessionIdFromFilename(filePath) {
94
+ const base = path.basename(filePath, ".jsonl");
95
+ // Layout: <timestamp>_<sessionId>.jsonl — timestamp is ISO-like with dashes/colons
96
+ // replaced, then underscore, then the id. Prefer the suffix after the last "_".
97
+ const idx = base.lastIndexOf("_");
98
+ if (idx > 0 && idx < base.length - 1) return base.slice(idx + 1);
99
+ return base || null;
100
+ }
101
+
102
+ function mainSessionStem(filePath) {
103
+ return path.basename(filePath, ".jsonl");
104
+ }
105
+
106
+ function ompInventoryIncomplete(pathName, error) {
107
+ const err = new Error(`omp inventory incomplete: ${pathName}`);
108
+ err.code = "OMP_INVENTORY_INCOMPLETE";
109
+ err.path = pathName;
110
+ err.cause = error;
111
+ return err;
112
+ }
113
+
114
+ function listNestedJsonlSync(dir, { strict = false } = {}) {
115
+ const found = [];
116
+ const walk = (current) => {
117
+ let entries;
118
+ try {
119
+ entries = fs.readdirSync(current, { withFileTypes: true });
120
+ } catch (error) {
121
+ if (error?.code === "ENOENT") return;
122
+ if (strict) throw ompInventoryIncomplete(current, error);
123
+ return;
124
+ }
125
+ for (const entry of entries) {
126
+ const full = path.join(current, entry.name);
127
+ let isDir = entry.isDirectory();
128
+ let isFile = entry.isFile();
129
+ if (!isDir && !isFile) {
130
+ try {
131
+ const st = fs.statSync(full);
132
+ isDir = st.isDirectory();
133
+ isFile = st.isFile();
134
+ } catch (error) {
135
+ if (error?.code !== "ENOENT" && strict) throw ompInventoryIncomplete(full, error);
136
+ continue;
137
+ }
138
+ }
139
+ if (isDir) walk(full);
140
+ else if (isFile && entry.name.endsWith(".jsonl")) found.push(full);
141
+ }
142
+ };
143
+ walk(dir);
144
+ found.sort((a, b) => a.localeCompare(b));
145
+ return found;
146
+ }
147
+
148
+ function ompSubagentFilesForMain(mainPath, { strict = false } = {}) {
149
+ const nestedDir = mainPath.replace(/\.jsonl$/i, "");
150
+ if (!nestedDir || nestedDir === mainPath) return [];
151
+ try {
152
+ const st = fs.statSync(nestedDir);
153
+ if (!st.isDirectory()) return [];
154
+ } catch (error) {
155
+ if (error?.code === "ENOENT") return [];
156
+ if (strict) throw ompInventoryIncomplete(nestedDir, error);
157
+ return [];
158
+ }
159
+ return listNestedJsonlSync(nestedDir, { strict });
160
+ }
161
+
162
+ function ompSessionDependencies(mainFiles) {
163
+ const deps = new Set();
164
+ for (const file of mainFiles || []) {
165
+ deps.add(file);
166
+ for (const nested of ompSubagentFilesForMain(file)) deps.add(nested);
167
+ }
168
+ return [...deps];
169
+ }
170
+
171
+ function discoverOmpSessionFiles(env = process.env, incomplete = []) {
172
+ // Walk sessions/ ourselves so permission errors surface as incomplete
173
+ // discovery (preserving the prior sidecar) instead of looking like "no OMP".
174
+ const agentDir = resolveOmpAgentDir(env);
175
+ if (!agentDir) return [];
176
+ const sessionsDir = path.join(agentDir, "sessions");
177
+ let cwdEntries;
178
+ try {
179
+ cwdEntries = fs.readdirSync(sessionsDir);
180
+ } catch (error) {
181
+ if (error?.code === "ENOENT") return [];
182
+ incomplete.push({ path: sessionsDir, error: error?.code || "unreadable" });
183
+ return [];
184
+ }
185
+
186
+ const files = [];
187
+ for (const cwdDir of cwdEntries) {
188
+ const cwdPath = path.join(sessionsDir, cwdDir);
189
+ let stat;
190
+ try {
191
+ stat = fs.statSync(cwdPath);
192
+ } catch (error) {
193
+ if (error?.code !== "ENOENT") incomplete.push({ path: cwdPath, error: error?.code || "unreadable" });
194
+ continue;
195
+ }
196
+ if (!stat.isDirectory()) continue;
197
+ let entries;
198
+ try {
199
+ entries = fs.readdirSync(cwdPath);
200
+ } catch (error) {
201
+ if (error?.code !== "ENOENT") incomplete.push({ path: cwdPath, error: error?.code || "unreadable" });
202
+ continue;
203
+ }
204
+ for (const entry of entries) {
205
+ if (!entry.endsWith(".jsonl")) continue;
206
+ files.push(path.join(cwdPath, entry));
207
+ }
208
+ }
209
+ files.sort((a, b) => a.localeCompare(b));
210
+ return files;
211
+ }
212
+
213
+ function discoverOmpSubagentFiles(env = process.env) {
214
+ try {
215
+ return resolveOmpSubagentFiles(env);
216
+ } catch {
217
+ return [];
218
+ }
219
+ }
220
+
221
+ function fingerprintContent(content) {
222
+ // In-memory only: never written to the sidecar.
223
+ return crypto.createHash("sha256").update(JSON.stringify(content ?? null)).digest("hex");
224
+ }
225
+
226
+ function toolNameFromBlock(block) {
227
+ if (!block || typeof block !== "object") return "";
228
+ if (block.type !== "toolCall" && block.type !== "tool_use") return "";
229
+ const name = block.name || block.toolName || block.tool_name || "";
230
+ return typeof name === "string" ? name.trim().toLowerCase() : "";
231
+ }
232
+
233
+ async function readOmpSessionFiles(mainFiles, { seenEntryIds = new Set() } = {}) {
234
+ const rows = [];
235
+ const mains = [...new Set((mainFiles || []).filter(Boolean))].sort((a, b) => a.localeCompare(b));
236
+
237
+ for (const mainPath of mains) {
238
+ const nested = ompSubagentFilesForMain(mainPath, { strict: true });
239
+ const files = [mainPath, ...nested];
240
+ const tokens = emptyUsage();
241
+ const modelUsage = new Map();
242
+ const buckets = new Map();
243
+ const times = new Set();
244
+ let turns = 0;
245
+ let edits = 0;
246
+ let retries = 0;
247
+ let subagentCalls = 0;
248
+ const subagentTypes = new Map();
249
+ let cwd = null;
250
+ let title = null;
251
+ let sessionId = null;
252
+ let lastPrompt = null;
253
+ let turnOpen = false;
254
+ let hadEdit = false;
255
+ let turnModel = "omp-unknown";
256
+ const modelsForEdits = new Map();
257
+
258
+ const closeTurn = () => {
259
+ if (hadEdit) {
260
+ edits += 1;
261
+ modelsForEdits.set(turnModel, (modelsForEdits.get(turnModel) || 0) + 1);
262
+ }
263
+ hadEdit = false;
264
+ turnOpen = false;
265
+ };
266
+
267
+ for (const filePath of files) {
268
+ const isMain = filePath === mainPath;
269
+ // Isolate turn state per transcript so a nested child cannot rewrite the
270
+ // parent's open-turn edit attribution (or vice versa).
271
+ closeTurn();
272
+ turnModel = "omp-unknown";
273
+ let input;
274
+ try {
275
+ input = fs.createReadStream(filePath);
276
+ } catch (error) {
277
+ throw ompInventoryIncomplete(filePath, error);
278
+ }
279
+ try {
280
+ for await (const record of physicalJsonlRecords(input, {
281
+ invalidUtf8: "record",
282
+ maxRecordBytes: OMP_MAX_RECORD_BYTES,
283
+ })) {
284
+ // Match hourly scanOmpLikeTranscript: accept a complete final record
285
+ // even without a trailing newline; skip oversized / invalid UTF-8.
286
+ if (record.oversized || record.utf8Valid === false || !record.line) {
287
+ if (!record.terminated) break;
288
+ continue;
289
+ }
290
+ let entry;
291
+ try {
292
+ entry = JSON.parse(record.line);
293
+ } catch {
294
+ if (!record.terminated) break;
295
+ continue;
296
+ }
297
+ if (!entry || typeof entry !== "object") {
298
+ if (!record.terminated) break;
299
+ continue;
300
+ }
301
+
302
+ if (entry.type === "session") {
303
+ // Only the main transcript names the resumable session / project.
304
+ // Nested subagent headers carry their own ids and must not replace it.
305
+ if (isMain) {
306
+ if (typeof entry.id === "string" && entry.id.trim()) sessionId = entry.id.trim();
307
+ if (typeof entry.cwd === "string" && entry.cwd.trim()) cwd = entry.cwd.trim();
308
+ if (typeof entry.title === "string" && entry.title.trim()) title = entry.title.trim();
309
+ }
310
+ const ts = entryTimestampMs(entry, null);
311
+ if (ts != null) times.add(ts);
312
+ continue;
313
+ }
314
+
315
+ if (isMain && entry.type === "title_change" && typeof entry.title === "string" && entry.title.trim()) {
316
+ title = entry.title.trim();
317
+ continue;
318
+ }
319
+
320
+ // model_usage contributes accounting outside the transcript; include it
321
+ // so Sessions totals stay aligned with providers that emit those rows.
322
+ if (entry.type === "model_usage") {
323
+ const delta = ompUsage(entry.usage);
324
+ const ts = entryTimestampMs(entry, entry);
325
+ const model = modelAlias(entry.model) || turnModel || "omp-unknown";
326
+ const usageId = typeof entry.id === "string" && entry.id ? `model_usage:${entry.id}` : null;
327
+ if (delta && usageId && ts != null && !seenEntryIds.has(usageId)) {
328
+ seenEntryIds.add(usageId);
329
+ const modelRow = modelUsage.get(model) || { model, ...emptyUsage(), usage_events: 0 };
330
+ const bucketTime = new Date(Math.floor(ts / 60000) * 60000).toISOString();
331
+ const key = `${model}\0${bucketTime}`;
332
+ const bucket = buckets.get(key) || { model, timestamp: bucketTime, ...emptyUsage(), usage_events: 0 };
333
+ addUsage(tokens, delta);
334
+ addUsage(modelRow, delta);
335
+ addUsage(bucket, delta);
336
+ modelRow.usage_events += 1;
337
+ bucket.usage_events += 1;
338
+ modelUsage.set(model, modelRow);
339
+ buckets.set(key, bucket);
340
+ turnModel = model;
341
+ times.add(ts);
342
+ }
343
+ continue;
344
+ }
345
+
346
+ if (entry.type !== "message") continue;
347
+ const message = entry.message && typeof entry.message === "object" ? entry.message : null;
348
+ if (!message) continue;
349
+
350
+ // Entry-id dedup applies to activity metrics as well as tokens: forks
351
+ // and rewritten prefixes reuse the same ids across files.
352
+ const entryId = typeof entry.id === "string" && entry.id ? entry.id : null;
353
+ if (entryId && seenEntryIds.has(entryId)) continue;
354
+ if (entryId) seenEntryIds.add(entryId);
355
+
356
+ const ts = entryTimestampMs(entry, message);
357
+ if (ts != null) times.add(ts);
358
+
359
+ if (message.role === "user" && isMain) {
360
+ closeTurn();
361
+ turns += 1;
362
+ turnOpen = true;
363
+ const fingerprint = fingerprintContent(message.content ?? null);
364
+ if (lastPrompt && fingerprint === lastPrompt) retries += 1;
365
+ lastPrompt = fingerprint;
366
+ continue;
367
+ }
368
+
369
+ if (message.role === "assistant") {
370
+ const delta = ompUsage(message.usage);
371
+ const model = modelAlias(message.model) || turnModel || "omp-unknown";
372
+ // Hourly parser only counts assistants with an entry id.
373
+ if (delta && entryId && ts != null) {
374
+ const modelRow = modelUsage.get(model) || { model, ...emptyUsage(), usage_events: 0 };
375
+ const bucketTime = new Date(Math.floor(ts / 60000) * 60000).toISOString();
376
+ const key = `${model}\0${bucketTime}`;
377
+ const bucket = buckets.get(key) || { model, timestamp: bucketTime, ...emptyUsage(), usage_events: 0 };
378
+ addUsage(tokens, delta);
379
+ addUsage(modelRow, delta);
380
+ addUsage(bucket, delta);
381
+ modelRow.usage_events += 1;
382
+ bucket.usage_events += 1;
383
+ modelUsage.set(model, modelRow);
384
+ buckets.set(key, bucket);
385
+ turnModel = model;
386
+ }
387
+
388
+ const content = Array.isArray(message.content) ? message.content : [];
389
+ for (const block of content) {
390
+ const name = toolNameFromBlock(block);
391
+ if (!name) continue;
392
+ if (turnOpen && EDIT_TOOLS.has(name)) hadEdit = true;
393
+ if (AGENT_TOOLS.has(name)) {
394
+ subagentCalls += 1;
395
+ subagentTypes.set(name, (subagentTypes.get(name) || 0) + 1);
396
+ }
397
+ }
398
+ continue;
399
+ }
400
+ }
401
+ } catch (error) {
402
+ // Malformed records are skipped above; I/O / inventory errors must
403
+ // surface so the builder keeps the previous complete OMP snapshot.
404
+ if (error?.code === "OMP_INVENTORY_INCOMPLETE") throw error;
405
+ throw ompInventoryIncomplete(filePath, error);
406
+ } finally {
407
+ input.destroy?.();
408
+ }
409
+ closeTurn();
410
+ }
411
+
412
+ // Nested subagent transcripts are separate files under the main stem; each
413
+ // file counts as one observed child session even when toolCall names differ.
414
+ if (nested.length > 0) {
415
+ subagentCalls = Math.max(subagentCalls, nested.length);
416
+ for (const nestedPath of nested) {
417
+ const name = path.basename(nestedPath, ".jsonl").toLowerCase() || "subagent";
418
+ subagentTypes.set(name, (subagentTypes.get(name) || 0) + 1);
419
+ }
420
+ }
421
+
422
+ if (!sessionId) sessionId = sessionIdFromFilename(mainPath);
423
+ const orderedTimes = [...times].sort((a, b) => a - b);
424
+ let activeMs = 0;
425
+ for (let i = 1; i < orderedTimes.length; i++) {
426
+ const gap = orderedTimes[i] - orderedTimes[i - 1];
427
+ if (gap <= 30 * 60 * 1000) activeMs += gap;
428
+ }
429
+ const models = [...modelUsage.values()].map((row) => ({
430
+ ...row,
431
+ edit_turns: modelsForEdits.get(row.model) || 0,
432
+ }));
433
+ if (!tokens.total_tokens && !turns) continue;
434
+
435
+ rows.push({
436
+ identity: sessionId || mainSessionStem(mainPath),
437
+ files,
438
+ session_id: sessionId || null,
439
+ title,
440
+ project_ref: cwd,
441
+ model: models.length === 1 ? models[0].model : models.length > 1 ? "mixed" : "omp-unknown",
442
+ model_usage: models,
443
+ usage_buckets: [...buckets.values()],
444
+ tokens,
445
+ turns,
446
+ edit_turns: edits,
447
+ retry_turns: retries,
448
+ subagent_calls: subagentCalls,
449
+ subagent_types: Object.fromEntries([...subagentTypes.entries()].sort()),
450
+ usage_events: models.reduce((sum, row) => sum + count(row.usage_events), 0),
451
+ started_at: orderedTimes.length ? new Date(orderedTimes[0]).toISOString() : null,
452
+ ended_at: orderedTimes.length ? new Date(orderedTimes.at(-1)).toISOString() : null,
453
+ active_ms: activeMs,
454
+ provenance: {
455
+ source: "local-session-log",
456
+ confidence: "observed",
457
+ retry_confidence: "inferred",
458
+ content_retained: false,
459
+ usage: "omp-jsonl-metadata",
460
+ },
461
+ });
462
+ }
463
+
464
+ return rows;
465
+ }
466
+
467
+ async function readOmpSessions(mainFiles, options = {}) {
468
+ return readOmpSessionFiles(mainFiles, {
469
+ seenEntryIds: options.seenEntryIds instanceof Set ? options.seenEntryIds : new Set(),
470
+ });
471
+ }
472
+
473
+ module.exports = {
474
+ discoverOmpSessionFiles,
475
+ discoverOmpSubagentFiles,
476
+ ompSessionDependencies,
477
+ ompSubagentFilesForMain,
478
+ readOmpSessions,
479
+ sessionIdFromFilename,
480
+ };
@@ -183,6 +183,27 @@
183
183
  "cache_write": 0.2,
184
184
  "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."
185
185
  },
186
+ "qwen3.8-max": {
187
+ "input": 2,
188
+ "output": 6,
189
+ "cache_read": 0.17,
190
+ "cache_write": 2.5,
191
+ "note": "Alibaba Model Studio Singapore (International), USD per MTok, verified 2026-10-25: https://www.alibabacloud.com/help/en/model-studio/model-pricing and https://www.alibabacloud.com/help/en/model-studio/qwen3-8-max . Reference API estimate; other regions/providers can have different rates. Issue #744."
192
+ },
193
+ "qwen3.8-max-0902": {
194
+ "input": 2,
195
+ "output": 6,
196
+ "cache_read": 0.17,
197
+ "cache_write": 2.5,
198
+ "note": "Alibaba Model Studio Singapore (International), USD per MTok, verified 2026-10-25: https://www.alibabacloud.com/help/en/model-studio/model-pricing and https://www.alibabacloud.com/help/en/model-studio/qwen3-8-max . Reference API estimate; other regions/providers can have different rates. Issue #744. Dated SKU qwen3.8-max-0902 shares Singapore International list rates."
199
+ },
200
+ "qwen3.8-max-2026-09-02": {
201
+ "input": 2,
202
+ "output": 6,
203
+ "cache_read": 0.17,
204
+ "cache_write": 2.5,
205
+ "note": "Alibaba Model Studio Singapore (International), USD per MTok, verified 2026-10-25: https://www.alibabacloud.com/help/en/model-studio/qwen3-8-max . Alias of qwen3.8-max-0902; same Singapore International list rates. Issue #744."
206
+ },
186
207
  "hy4-preview": {
187
208
  "input": 0.833,
188
209
  "output": 2.5,
@@ -508,6 +529,20 @@
508
529
  "output": 0,
509
530
  "cache_read": 0
510
531
  },
532
+ "muse-spark-1.2-contributor-free": {
533
+ "input": 0,
534
+ "output": 0,
535
+ "cache_read": 0,
536
+ "cache_write": 0,
537
+ "note": "Muse contributor free SKU. Also covered by matcher -free suffix; exact entry keeps status=free (issue #744)."
538
+ },
539
+ "muse-spark-1.3-contributor-free": {
540
+ "input": 0,
541
+ "output": 0,
542
+ "cache_read": 0,
543
+ "cache_write": 0,
544
+ "note": "Muse contributor free SKU. Also covered by matcher -free suffix; exact entry keeps status=free (issue #744)."
545
+ },
511
546
  "minimax-m2.1-free": {
512
547
  "input": 0,
513
548
  "output": 0,
@@ -252,6 +252,11 @@ function lookupContainedExactCaseInsensitive(table, model) {
252
252
  // is a separate SKU from Opus 5). Dated and provider suffixes still work.
253
253
  const suffix = lower.slice(start + key.length);
254
254
  if (/^[.\d]/.test(suffix)) continue;
255
+ // Alternate SKU markers (preview/next/exp/…) must not inherit the base
256
+ // exact rate, including when they follow a date snapshot
257
+ // (`qwen3.8-max-2026-09-02-preview`, `qwen3-8-max-0902-preview`).
258
+ // Plain dated (-0902 / -YYYY-MM-DD) and provider-qualified suffixes still match.
259
+ if (/(?:^|-)(?:preview|next|exp|experimental|beta|alpha)(?:-|$)/.test(suffix)) continue;
255
260
  return table[key];
256
261
  }
257
262
  return null;
@@ -280,15 +285,6 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
280
285
  value: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
281
286
  };
282
287
  }
283
- // OpenCode Zen free models carry a `-free` suffix on the paid model id
284
- // (`deepseek-v4.1-flash-free`); the base-name fuzzy match must not bill them.
285
- if (sourceKey === "opencode" && lower.endsWith("-free")) {
286
- return {
287
- hit: true,
288
- source: "curated:opencode-free-suffix",
289
- value: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
290
- };
291
- }
292
288
  // AStudio does not disclose its routed model. Stop before generic aliases
293
289
  // and fuzzy matching can turn an unresolved router into a priced model.
294
290
  if (sourceKey === "acode" && (lower === "auto" || lower.endsWith("-auto"))) {
@@ -302,12 +298,8 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
302
298
  }
303
299
  }
304
300
 
305
- // 1. CURATED exact
306
- // Qwen Flash's provider-qualified and dated IDs share the reference SKU.
307
- // Keep the boundary strict: Flash-Next and Max have independent prices.
308
- if (/(?:^|\/)qwen3[.-]8-flash(?:-\d{4}-\d{2}-\d{2})?$/.test(lower.trim()) && curated.exact?.["qwen3.8-flash"]) {
309
- return { hit: true, source: "curated:exact", value: curated.exact["qwen3.8-flash"] };
310
- }
301
+ // 1. CURATED exact (before family regex aliases so a distinct dated exact
302
+ // entry cannot be overridden by the shared-SKU regex).
311
303
  if (curated.exact && curated.exact[lookupModel]) {
312
304
  return { hit: true, source: "curated:exact", value: curated.exact[lookupModel] };
313
305
  }
@@ -316,6 +308,17 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
316
308
  return { hit: true, source: "curated:exact-dot", value: curatedDotExact };
317
309
  }
318
310
 
311
+ // Qwen Flash's provider-qualified and dated IDs share the reference SKU.
312
+ // Keep the boundary strict: Flash-Next and Max have independent prices.
313
+ if (/(?:^|\/)qwen3[.-]8-flash(?:-\d{4}-\d{2}-\d{2})?$/.test(lower.trim()) && curated.exact?.["qwen3.8-flash"]) {
314
+ return { hit: true, source: "curated:exact", value: curated.exact["qwen3.8-flash"] };
315
+ }
316
+ // Qwen Max (dated -0902 and ISO snapshot -YYYY-MM-DD) share Singapore International SKU.
317
+ // Keep the boundary strict vs Flash / Flash-Next / -preview (issue #744).
318
+ if (/(?:^|\/)qwen3[.-]8-max(?:-0902|-\d{4}-\d{2}-\d{2})?$/.test(lower.trim()) && curated.exact?.["qwen3.8-max"]) {
319
+ return { hit: true, source: "curated:exact", value: curated.exact["qwen3.8-max"] };
320
+ }
321
+
319
322
  // 2. LiteLLM exact
320
323
  if (litellm && litellm[lookupModel]) {
321
324
  return { hit: true, source: "litellm:exact", value: litellm[lookupModel] };
@@ -324,6 +327,32 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
324
327
  if (litellmDotExact) {
325
328
  return { hit: true, source: "litellm:exact-dot", value: litellmDotExact };
326
329
  }
330
+
331
+ // Free gateway SKUs carry a `-free` suffix on the paid model id
332
+ // (`deepseek-v4.1-flash-free`, `minimax-m2.5-free`, Muse `*-contributor-free`).
333
+ // Runs AFTER curated/LiteLLM exact so an explicit paid exact entry still wins,
334
+ // and BEFORE alias/fuzzy/base-name matchers so `foo-free` cannot inherit `foo`
335
+ // list rates. OpenCode Zen established the convention; other sources reuse it
336
+ // when they emit the same suffix (issue #744).
337
+ if (lower.endsWith("-free")) {
338
+ return {
339
+ hit: true,
340
+ source: sourceKey === "opencode" ? "curated:opencode-free-suffix" : "curated:free-suffix",
341
+ value: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
342
+ };
343
+ }
344
+
345
+ // Kimi Code K2.8 Preview aliases must not inherit fuzzy `kimi` → kimi-k2.5 rates (#766).
346
+ // Parser stores bare `k2.8`; raw `kimicoding` / `kimi-k2.8` / `kimicoding/k2.8` may still
347
+ // appear in lookups. `kimi-for-coding` keeps its curated exact entry (resolved above).
348
+ // Stay unpriced until an official per-token public rate exists.
349
+ if (
350
+ /(?:^|\/)(?:kimicoding(?:\/k2\.8)?|kimi-k2\.8|k2\.8)(?:-|\.|$)/.test(lower.trim()) ||
351
+ lower.trim() === "kimicoding"
352
+ ) {
353
+ return { hit: false, source: "miss", value: null };
354
+ }
355
+
327
356
  const curatedDotContainedExact = lookupContainedExactCaseInsensitive(curated.exact, dotForm);
328
357
  if (curatedDotContainedExact) {
329
358
  return { hit: true, source: "curated:exact-dot", value: curatedDotContainedExact };