tokentracker-cli 0.96.1 → 0.96.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.de.md +34 -25
  2. package/README.ja.md +38 -27
  3. package/README.ko.md +39 -28
  4. package/README.md +36 -28
  5. package/README.zh-CN.md +36 -25
  6. package/dashboard/dist/assets/{AchievementBadge-DKyElm0T.js → AchievementBadge-BX4iBBYM.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BvCx3wND.js → AchievementsPage-DRYRuCbh.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DN69LzXX.js → AchievementsSection-BM_JWWhJ.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-DGiKdZfz.js → BadgeDetailModal-DxoxXst2.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-zN8rH7_K.js → BotAnimated-BVpFxG8u.js} +1 -1
  11. package/dashboard/dist/assets/{Card-COWo-ClV.js → Card-BJJs701t.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BWsB7Eat.js → ClawdAnimated-46wPZ_Tc.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-BfvIwuoJ.js → CommandPalette-B_zkc7IU.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-BQWg7R_y.js → CommunityStatsModal-M2k5rIf1.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-C7ABSI-O.js → ConfirmModal-DBvOTcAQ.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-vMzEZzej.js → Controls-BCkjPMSH.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-3bHhou09.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-3ejUBLWM.js → DevicePage-DFBUce8C.js} +1 -1
  19. package/dashboard/dist/assets/DialogClose-kvDd8h3V.js +1 -0
  20. package/dashboard/dist/assets/DialogDescription-BMyOicfz.js +1 -0
  21. package/dashboard/dist/assets/DialogTitle-DL5nvD31.js +1 -0
  22. package/dashboard/dist/assets/{FadeIn-D7Jcfz1N.js → FadeIn-B-Bg6SEy.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-D--Rx5BS.js → HeaderGithubStar-BExz0hh4.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-n1W0z3Wr.js → HoverTooltip-Dj5zemYT.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-D202tl9B.js → IpCheckPage-DN7FuJ_v.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-D6yL2EZo.js → LandingPage-9dKgSBRy.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-ChFg2mtA.js → LeaderboardAvatar-00V95XIg.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-BXHwGs8r.js → LeaderboardPage-DkKfUt0Z.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-B5LoOpCk.js → LeaderboardProfileModal-C9sIHBgN.js} +4 -4
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-jsukhRz8.js → LeaderboardProfilePage-BAiLMsEb.js} +1 -1
  31. package/dashboard/dist/assets/LimitsPage-BuenTaHa.js +8 -0
  32. package/dashboard/dist/assets/{LocalOnlyNotice-Du0lz657.js → LocalOnlyNotice-CKhY27Lr.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-BFEX9x7-.js → LoginCard-BbvoBx5N.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-DqC7_d35.js → LoginModal-P7zIkBhO.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-Dzx8N7HE.js → LoginPage-Bo0bR26x.js} +1 -1
  36. package/dashboard/dist/assets/PetPage-C6i9bSSl.js +1 -0
  37. package/dashboard/dist/assets/PopoverPopup-h0gf36vg.js +1 -0
  38. package/dashboard/dist/assets/ProviderIcon-Dm8fwPdj.js +1 -0
  39. package/dashboard/dist/assets/{ResetPasswordPage-x_GR79sR.js → ResetPasswordPage-DvSJ8kg0.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-BxDiDV4R.js → SegmentedControl-CP7oiGZE.js} +1 -1
  41. package/dashboard/dist/assets/Select-iwP92x9C.js +1 -0
  42. package/dashboard/dist/assets/SelectItemText-DzljA15_.js +1 -0
  43. package/dashboard/dist/assets/{ServiceStatusPage-BVQuVdjg.js → ServiceStatusPage-uD4D0Tid.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-DE39wNIO.js → SessionsPage-Cmb1ssNP.js} +2 -2
  45. package/dashboard/dist/assets/SettingsPage-D8ryg1Pv.js +2 -0
  46. package/dashboard/dist/assets/SkillsPage-eXI09q03.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-BPM779Wb.js → TokenGalaxy-f2djrlUv.js} +1 -1
  48. package/dashboard/dist/assets/ToolbarRootContext-CDxCPYLv.js +1 -0
  49. package/dashboard/dist/assets/{TrendMonitor-BkNy-o1D.js → TrendMonitor-C419X22G.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-Bpg6H-ZB.js → WidgetsPage-AYq__pOO.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-C6-NM-RO.js → WrappedPage-Bsm9k5XI.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-CvMMIjhi.css → __vite-browser-external-5P7me0Qq.css} +1 -1
  53. package/dashboard/dist/assets/__vite-browser-external-BDkeh90N.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-DpbkRqXT.js → agent-logos-B6Vg-4_1.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-BjOtWUVs.js → arrow-up-right-DIQXwaOD.js} +1 -1
  56. package/dashboard/dist/assets/{check-BlxnX5jZ.js → check-CgD_bV39.js} +1 -1
  57. package/dashboard/dist/assets/composite-BIgEqWZD.js +1 -0
  58. package/dashboard/dist/assets/{copy-CIsWjLY5.js → copy-DlM1BHdp.js} +1 -1
  59. package/dashboard/dist/assets/{download-QDaQbQdd.js → download-CQFG-VIY.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-DPV4VU3m.js → external-link-BQyDqGxz.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B_X-7VNp.js → flame-BamzyXzF.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-DEx2TJj8.js → format-tokens-C4G-_xYt.js} +1 -1
  63. package/dashboard/dist/assets/{icons-983RyGSl.js → icons-Ntscvl1a.js} +1 -1
  64. package/dashboard/dist/assets/{index-qtonw_x4.js → index-CUrcPqgp.js} +1 -1
  65. package/dashboard/dist/assets/{index-BONYA8HZ.js → index-DGxgsNoQ.js} +1 -1
  66. package/dashboard/dist/assets/{index-Cjs5stC0.js → index-FRIt5mlv.js} +1 -1
  67. package/dashboard/dist/assets/{info-DpVOhXZI.js → info-CXytWcZi.js} +1 -1
  68. package/dashboard/dist/assets/{link-2-CBro-upz.js → link-2-Dexoo_jO.js} +1 -1
  69. package/dashboard/dist/assets/{loader-circle-wYFdSyOd.js → loader-circle-Cn0JHc8t.js} +1 -1
  70. package/dashboard/dist/assets/{maximize-2-DhTyQ4ly.js → maximize-2-DLh7gYuP.js} +1 -1
  71. package/dashboard/dist/assets/{modifiers.esm-CPljwVhN.js → modifiers.esm-DOh_xtyt.js} +1 -1
  72. package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
  73. package/dashboard/dist/assets/{monitor-smartphone-D_wr-HOR.js → monitor-smartphone-CpqS9Ior.js} +1 -1
  74. package/dashboard/dist/assets/{plus-BNjfkBEF.js → plus-bfQV7nyv.js} +1 -1
  75. package/dashboard/dist/assets/provider-display-Bf9pW06U.js +42 -0
  76. package/dashboard/dist/assets/{react-yHv9MTUA.js → react-BmFZKA_b.js} +1 -1
  77. package/dashboard/dist/assets/{search-Dfak92ZA.js → search-l3jJ2fzC.js} +1 -1
  78. package/dashboard/dist/assets/{skills-api-By2PdNa1.js → skills-api-CWmLnaxn.js} +1 -1
  79. package/dashboard/dist/assets/{sortable.esm-B9xy66_v.js → sortable.esm-DLgyXLHM.js} +1 -1
  80. package/dashboard/dist/assets/store-JM-a8ElZ.js +1 -0
  81. package/dashboard/dist/assets/{terminal-pbXWBmZM.js → terminal-6aY2Sn8P.js} +1 -1
  82. package/dashboard/dist/assets/{trash-2-CboDPwbW.js → trash-D5oon5pT.js} +1 -1
  83. package/dashboard/dist/assets/{use-community-stats-D6HTdnHq.js → use-community-stats-ZWUMFwzc.js} +1 -1
  84. package/dashboard/dist/assets/{use-latest-request-guard-hwSr9Cam.js → use-latest-request-guard-BDaXr4gK.js} +1 -1
  85. package/dashboard/dist/assets/use-limits-display-prefs-yXXToNIc.js +1 -0
  86. package/dashboard/dist/assets/{use-ordered-list-OtfOqrNb.js → use-ordered-list-C6RG8N6c.js} +1 -1
  87. package/dashboard/dist/assets/use-reduced-motion-BqDKim_3.js +1 -0
  88. package/dashboard/dist/assets/{use-session-efficiency-pref-BScvtS8F.js → use-session-efficiency-pref-DpgZO_Lx.js} +1 -1
  89. package/dashboard/dist/assets/{use-transform-Cy1ykjEs.js → use-transform-D42P7jkd.js} +1 -1
  90. package/dashboard/dist/assets/use-usage-limits-CV6puFwq.js +1 -0
  91. package/dashboard/dist/assets/{useCurrency-VqF3cy1C.js → useCurrency-GWBmOrCR.js} +1 -1
  92. package/dashboard/dist/assets/{useTokenFormat-Cgub4MXu.js → useTokenFormat-Beybkb0v.js} +1 -1
  93. package/dashboard/dist/assets/{zap-DStzvQJI.js → zap-CEq2616W.js} +1 -1
  94. package/dashboard/dist/brand-logos/acode-dark.png +0 -0
  95. package/dashboard/dist/brand-logos/acode.png +0 -0
  96. package/dashboard/dist/brand-logos/commandcode.svg +1 -0
  97. package/dashboard/dist/brand-logos/devin.svg +6 -0
  98. package/dashboard/dist/index.html +20 -16
  99. package/dashboard/dist/ip-check.html +2 -2
  100. package/dashboard/dist/leaderboard.html +2 -2
  101. package/dashboard/dist/llms.txt +4 -2
  102. package/dashboard/dist/share.html +2 -2
  103. package/package.json +2 -2
  104. package/src/commands/init.js +114 -4
  105. package/src/commands/status.js +37 -0
  106. package/src/commands/sync.js +92 -10
  107. package/src/commands/uninstall.js +23 -0
  108. package/src/lib/codex-config.js +42 -0
  109. package/src/lib/codex-rollout-parser.js +2 -2
  110. package/src/lib/commandcode-limits.js +317 -0
  111. package/src/lib/devin-limits.js +249 -0
  112. package/src/lib/devin-usage.js +212 -0
  113. package/src/lib/local-api.js +14 -0
  114. package/src/lib/passive-mode.js +13 -1
  115. package/src/lib/pricing/curated-overrides.json +78 -1
  116. package/src/lib/pricing/index.js +11 -9
  117. package/src/lib/pricing/matcher.js +28 -0
  118. package/src/lib/pricing/seed-snapshot.json +1 -1
  119. package/src/lib/rollout.js +1473 -147
  120. package/src/lib/session-analytics.js +2 -2
  121. package/src/lib/skills-manager.js +5 -0
  122. package/src/lib/subscriptions.js +70 -16
  123. package/src/lib/usage-limits.js +240 -49
  124. package/dashboard/dist/assets/DashboardPage-QBhbCMZF.js +0 -1
  125. package/dashboard/dist/assets/DialogClose-D_dvtfGV.js +0 -1
  126. package/dashboard/dist/assets/DialogDescription-ryGYdQjX.js +0 -1
  127. package/dashboard/dist/assets/DialogTitle-_ahf0z8b.js +0 -1
  128. package/dashboard/dist/assets/LimitsPage-PvqkOOwp.js +0 -6
  129. package/dashboard/dist/assets/PetPage-CNlWHF4z.js +0 -1
  130. package/dashboard/dist/assets/PopoverPopup-D9jadD3j.js +0 -1
  131. package/dashboard/dist/assets/ProviderIcon-Caz5PuAm.js +0 -1
  132. package/dashboard/dist/assets/Select-B5o-1sEl.js +0 -1
  133. package/dashboard/dist/assets/SelectItemText-CiA6L7Wk.js +0 -1
  134. package/dashboard/dist/assets/SettingsPage-DJrb_fsK.js +0 -2
  135. package/dashboard/dist/assets/SkillsPage-COk-YQQ5.js +0 -1
  136. package/dashboard/dist/assets/ToolbarRootContext-CozJT6Vs.js +0 -1
  137. package/dashboard/dist/assets/__vite-browser-external-BGnq8Ebm.js +0 -45
  138. package/dashboard/dist/assets/composite-Dxmivlk4.js +0 -1
  139. package/dashboard/dist/assets/limits-providers-BUoquSA-.js +0 -1
  140. package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
  141. package/dashboard/dist/assets/provider-display-BOBtUyFi.js +0 -42
  142. package/dashboard/dist/assets/store-oQZR4d9M.js +0 -1
  143. package/dashboard/dist/assets/use-limits-display-prefs-B-pbwUS8.js +0 -1
  144. package/dashboard/dist/assets/use-reduced-motion-DGXcZBX3.js +0 -1
  145. package/dashboard/dist/assets/use-usage-limits-CSMb2zYW.js +0 -1
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+
3
+ // Devin CLI (Cognition — devin.ai) local history projection.
4
+ //
5
+ // Devin persists chat history in a SQLite database at
6
+ // `$XDG_DATA_HOME/devin/cli/sessions.db` (default
7
+ // `~/.local/share/devin/cli/sessions.db`). Token usage lives on assistant
8
+ // message nodes inside the `chat_message` JSON document. The SQL below is a
9
+ // deliberately narrow projection: request identity, generation model, the
10
+ // original generation timestamps, the numeric usage metrics, and the owning
11
+ // session's `working_directory`/`created_at` for canonical ordering and local
12
+ // project attribution. Message bodies, prompt text, node `metadata`,
13
+ // `sessions.cogs_json`, and any credential-bearing state are never selected.
14
+ //
15
+ // Verified against devin CLI 3000.10.21 (611c1cba): replay/fork/compaction
16
+ // produces multiple `message_nodes` rows that share one
17
+ // `metadata.request_id` with identical metrics and original timestamps, so
18
+ // `request_id` — never `row_id` or `(session_id, request_id)` — is the
19
+ // canonical usage identity across the whole local install.
20
+ //
21
+ // Token components are disjoint: the DB's `metrics.input_tokens` already
22
+ // excludes `cache_read_tokens` and `cache_creation_tokens`, so each reported
23
+ // counter is added once and caches are never subtracted from input.
24
+
25
+ const DEVIN_TABLE_PROBE_SQL =
26
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('message_nodes','sessions')";
27
+
28
+ const metricField = (jsonPath, alias) =>
29
+ `CASE WHEN json_valid(n.chat_message) THEN json_extract(n.chat_message, '${jsonPath}') ELSE NULL END AS ${alias}`;
30
+
31
+ // `sessions` is LEFT JOINed only for `working_directory` and `created_at`;
32
+ // older databases may not have the table, in which case the projection runs
33
+ // without it (project attribution and session-age ordering degrade to nulls).
34
+ function devinUsageSql({ hasSessionsTable = true } = {}) {
35
+ const join = hasSessionsTable ? "LEFT JOIN sessions s ON s.id = n.session_id" : "";
36
+ const sessionCreatedAt = hasSessionsTable ? "s.created_at" : "NULL";
37
+ const workingDirectory = hasSessionsTable ? "s.working_directory" : "NULL";
38
+ return `
39
+ SELECT
40
+ n.row_id AS row_id,
41
+ n.session_id AS session_id,
42
+ ${sessionCreatedAt} AS session_created_at,
43
+ ${workingDirectory} AS working_directory,
44
+ ${metricField("$.metadata.request_id", "request_id")},
45
+ ${metricField("$.metadata.generation_model", "generation_model")},
46
+ ${metricField("$.metadata.started_generation_at", "started_generation_at")},
47
+ ${metricField("$.metadata.created_at", "message_created_at")},
48
+ ${metricField("$.metadata.metrics.input_tokens", "input_tokens")},
49
+ ${metricField("$.metadata.metrics.output_tokens", "output_tokens")},
50
+ ${metricField("$.metadata.metrics.cache_read_tokens", "cache_read_tokens")},
51
+ ${metricField("$.metadata.metrics.cache_creation_tokens", "cache_creation_tokens")}
52
+ FROM message_nodes n
53
+ ${join}
54
+ WHERE json_valid(n.chat_message)
55
+ AND json_extract(n.chat_message, '$.role') = 'assistant'
56
+ AND json_extract(n.chat_message, '$.metadata.request_id') IS NOT NULL
57
+ ORDER BY n.row_id
58
+ `.trim();
59
+ }
60
+
61
+ function normalizeText(value) {
62
+ if (typeof value !== "string") return null;
63
+ const trimmed = value.trim();
64
+ return trimmed.length > 0 ? trimmed : null;
65
+ }
66
+
67
+ // Strict integer check: usage fields must be finite non-negative safe
68
+ // integers. `null`/`undefined` cache counters are legal (no reported cache
69
+ // contribution) — callers pass them through `optionalCount`; required
70
+ // counters go through `requiredCount`.
71
+ function toSafeCount(value) {
72
+ const n = Number(value);
73
+ return Number.isSafeInteger(n) && n >= 0 ? n : null;
74
+ }
75
+
76
+ function requiredCount(value) {
77
+ // A missing/NULL required counter is incomplete usage, not zero — pending
78
+ // assistant records carry no metrics block and must stay uncounted until
79
+ // the writer finalizes them.
80
+ if (value === null || value === undefined) return null;
81
+ return toSafeCount(value);
82
+ }
83
+
84
+ function optionalCount(value) {
85
+ if (value === null || value === undefined) return 0;
86
+ return toSafeCount(value);
87
+ }
88
+
89
+ // RFC3339 timestamps arrive with both `Z` and `+00:00` spellings; Date.parse
90
+ // normalizes both to the same instant. Returns epoch ms or 0 when missing or
91
+ // unparsable.
92
+ function parseDevinTimestampMs(value) {
93
+ if (typeof value !== "string" || !value.trim()) return 0;
94
+ const ms = Date.parse(value.trim());
95
+ return Number.isFinite(ms) && ms > 0 ? ms : 0;
96
+ }
97
+
98
+ // session.created_at is Unix seconds; node row ordering uses ms.
99
+ function epochSecondsToMs(value) {
100
+ const n = Number(value);
101
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) * 1000 : null;
102
+ }
103
+
104
+ // Normalize one projected row into a canonical candidate, or null when the
105
+ // record is pending/malformed: missing request identity, missing/invalid
106
+ // numeric usage, or no usable original timestamp. Such rows are re-evaluated
107
+ // on every scan, so a record that gains metrics or a timestamp later is
108
+ // counted then.
109
+ function normalizeDevinUsageRow(row) {
110
+ const requestId = normalizeText(row?.request_id);
111
+ if (!requestId) return null;
112
+
113
+ const input = requiredCount(row?.input_tokens);
114
+ const output = requiredCount(row?.output_tokens);
115
+ const cacheRead = optionalCount(row?.cache_read_tokens);
116
+ const cacheCreation = optionalCount(row?.cache_creation_tokens);
117
+ if (input === null || output === null || cacheRead === null || cacheCreation === null) {
118
+ return null;
119
+ }
120
+
121
+ // The original generation time decides the bucket; the node insertion time
122
+ // (n.created_at) is clone/replay insertion time and is never a fallback.
123
+ const tsMs =
124
+ parseDevinTimestampMs(row?.started_generation_at) ||
125
+ parseDevinTimestampMs(row?.message_created_at);
126
+ if (!tsMs) return null;
127
+
128
+ return {
129
+ requestId,
130
+ sessionId: normalizeText(row?.session_id) || "",
131
+ rowId: toSafeCount(row?.row_id) ?? 0,
132
+ sessionCreatedAtMs: epochSecondsToMs(row?.session_created_at),
133
+ workingDirectory: normalizeText(row?.working_directory),
134
+ model: normalizeText(row?.generation_model) || "unknown",
135
+ tsMs,
136
+ input,
137
+ output,
138
+ cacheRead,
139
+ cacheCreation,
140
+ };
141
+ }
142
+
143
+ // Pick the canonical retained record for one request_id. Copies created by
144
+ // replay/fork/compaction carry identical original timestamps and metrics, so
145
+ // the order is: earliest original generation time, then the earliest-created
146
+ // owning session (a fork's copy lives in a younger session), then session id,
147
+ // then newest row first so an in-place correction appended as a fresh node
148
+ // inside the same session wins over the stale sibling.
149
+ function compareCandidates(a, b) {
150
+ if (a.tsMs !== b.tsMs) return a.tsMs - b.tsMs;
151
+ const sessionA = a.sessionCreatedAtMs ?? Number.POSITIVE_INFINITY;
152
+ const sessionB = b.sessionCreatedAtMs ?? Number.POSITIVE_INFINITY;
153
+ if (sessionA !== sessionB) return sessionA - sessionB;
154
+ if (a.sessionId !== b.sessionId) return a.sessionId < b.sessionId ? -1 : 1;
155
+ return b.rowId - a.rowId;
156
+ }
157
+
158
+ // Build the deduplicated per-request event list. Conversation ownership and
159
+ // session/project attribution are decided by the persisted request ledger in
160
+ // parseDevinIncremental — the projection only reports each request's
161
+ // best-evidence retained record; `conversation_count` stays 0 here and is
162
+ // filled from ledger state at reconcile time.
163
+ function buildDevinUsageEvents(rows) {
164
+ const byRequest = new Map();
165
+ let rowsSeen = 0;
166
+ let rowsSkipped = 0;
167
+ for (const row of Array.isArray(rows) ? rows : []) {
168
+ rowsSeen += 1;
169
+ const candidate = normalizeDevinUsageRow(row);
170
+ if (!candidate) {
171
+ rowsSkipped += 1;
172
+ continue;
173
+ }
174
+ const list = byRequest.get(candidate.requestId);
175
+ if (list) list.push(candidate);
176
+ else byRequest.set(candidate.requestId, [candidate]);
177
+ }
178
+
179
+ const events = [];
180
+ for (const [requestId, candidates] of byRequest) {
181
+ candidates.sort(compareCandidates);
182
+ const canonical = candidates[0];
183
+ const total = canonical.input + canonical.output + canonical.cacheRead + canonical.cacheCreation;
184
+ events.push({
185
+ requestId,
186
+ sessionId: canonical.sessionId,
187
+ workingDirectory: canonical.workingDirectory,
188
+ model: canonical.model,
189
+ tsMs: canonical.tsMs,
190
+ totals: {
191
+ input_tokens: canonical.input,
192
+ cached_input_tokens: canonical.cacheRead,
193
+ cache_creation_input_tokens: canonical.cacheCreation,
194
+ output_tokens: canonical.output,
195
+ reasoning_output_tokens: 0,
196
+ total_tokens: total,
197
+ billable_total_tokens: total,
198
+ total_cost_usd: 0,
199
+ conversation_count: 0,
200
+ },
201
+ });
202
+ }
203
+ events.sort((a, b) => a.tsMs - b.tsMs || (a.requestId < b.requestId ? -1 : 1));
204
+
205
+ return { events, rowsSeen, rowsSkipped };
206
+ }
207
+
208
+ module.exports = {
209
+ DEVIN_TABLE_PROBE_SQL,
210
+ devinUsageSql,
211
+ buildDevinUsageEvents,
212
+ };
@@ -3215,6 +3215,19 @@ function createLocalApiHandler({ queuePath }) {
3215
3215
  if (p === "/functions/tokentracker-usage-limits") {
3216
3216
  const { getUsageLimits, resetUsageLimitsCache } = require("./usage-limits");
3217
3217
  try {
3218
+ // Devin quota is opt-in (Settings > Usage & Limits > Providers). An
3219
+ // explicit devin=1 without local authentication is rejected before any
3220
+ // cache reset or provider work — silently downgrading it to a disabled
3221
+ // response would misreport an enabled client as a disabled provider.
3222
+ // Authorization is evaluated unconditionally so the check never depends
3223
+ // on the user-controlled opt-in flag itself.
3224
+ const localAuthorized = isAuthorizedLocalMutation(req);
3225
+ const devinParam = url.searchParams.get("devin");
3226
+ const devinEnabled = devinParam === "1" || devinParam === "true";
3227
+ if (devinEnabled && !localAuthorized) {
3228
+ json(res, { error: "Unauthorized" }, 401);
3229
+ return true;
3230
+ }
3218
3231
  const refreshParam = url.searchParams.get("refresh");
3219
3232
  const forceRefresh = refreshParam === "1" || refreshParam === "true";
3220
3233
  if (forceRefresh) {
@@ -3227,6 +3240,7 @@ function createLocalApiHandler({ queuePath }) {
3227
3240
  // Punches through the Claude disk fresh-cache (but not the 429
3228
3241
  // cooldown) — an explicit user refresh should hit upstream.
3229
3242
  forceRefresh,
3243
+ devinEnabled,
3230
3244
  });
3231
3245
  json(res, data);
3232
3246
  } catch (e) {
@@ -89,7 +89,7 @@ function classifyWritableFailure(filePath) {
89
89
  * every_code, opencode, openclaw, codebuddy, workbuddy, grok }
90
90
  * @returns {PassiveProvider[]}
91
91
  */
92
- function detectPassiveProviders({ home, hookStatus }) {
92
+ function detectPassiveProviders({ home, hookStatus, env = process.env }) {
93
93
  const out = [];
94
94
 
95
95
  // Claude Code — logs at ~/.claude/projects/<name>/*.jsonl
@@ -128,6 +128,18 @@ function detectPassiveProviders({ home, hookStatus }) {
128
128
  settingsPath: path.join(home, ".codex", "config.toml"),
129
129
  }));
130
130
 
131
+ const acodeHome = env.TOKENTRACKER_ACODE_HOME || path.join(home, ".acode");
132
+ out.push(buildEntry({
133
+ name: "acode",
134
+ hookExpected: true,
135
+ hookInstalled: Boolean(hookStatus?.acode_notify ?? hookStatus?.acode),
136
+ logsDir: acodeHome,
137
+ logsPredicate: (_full, name, isDir) =>
138
+ (isDir && (name === "sessions" || name === "archived_sessions")) ||
139
+ (!isDir && name.endsWith(".jsonl")),
140
+ settingsPath: path.join(acodeHome, "config.toml"),
141
+ }));
142
+
131
143
  // Every Code — same shape as Codex, different home
132
144
  out.push(buildEntry({
133
145
  name: "every_code",
@@ -2,7 +2,8 @@
2
2
  "_meta": {
3
3
  "note": "Curated price overrides. Always wins over LiteLLM. Two reasons to live here: (1) self-defined alias names that LiteLLM will never carry (kiro-*, hy3-*, composer-*, kimi-for-coding, free-tier OpenRouter routes); (2) prices we want to pin even if LiteLLM has the model (e.g. cache_write fields LiteLLM often omits). Units: USD per million tokens. Edit this file to override pricing without redeploying.",
4
4
  "units": "usd_per_million_tokens",
5
- "deepseek_v4_pricing_note": "Official DeepSeek V4 pricing verified on 2026-08-21. Peak hours are 01:00-04:00 and 06:00-10:00 UTC; all other hours are off-peak at half price. Exact entries store peak rates. computeRowCost applies the time-of-use discount from each row's UTC hour_start."
5
+ "deepseek_v4_pricing_note": "Official DeepSeek V4 pricing verified on 2026-08-21. Peak hours are 01:00-04:00 and 06:00-10:00 UTC; all other hours are off-peak at half price. Exact entries store peak rates. computeRowCost applies the time-of-use discount from each row's UTC hour_start.",
6
+ "iflytek_maas_pricing_note": "iFlytek MaaS inference prices supplied for AStudio on 2026-08-31. AStudio homepage: https://agent.xfyun.cn/. Official pricing source: https://maas.xfyun.cn/modelSquare. Source values are RMB per million tokens, converted at 7.2 RMB/USD, then rounded to two decimal places for billing. Models without a dedicated cache-hit price use the normal input rate; cache writes also use the normal input rate. iFlytek MaaS prices are source-scoped and do not inherit DeepSeek time-of-use discounts."
6
7
  },
7
8
  "exact": {
8
9
  "claude-fable-5": {
@@ -47,6 +48,13 @@
47
48
  "cache_write": 1.25,
48
49
  "note": "Canonical undated alias emitted by Cursor. Pin to the same rates as claude-haiku-4-5-20251001 so local cost does not depend on which dated LiteLLM aliases are present in the current cache."
49
50
  },
51
+ "gpt-6-astra": {
52
+ "input": 10,
53
+ "output": 50,
54
+ "cache_read": 1,
55
+ "cache_write": 12.5,
56
+ "note": "Official Standard USD/MTok rates verified 2026-09-07: https://developers.openai.com/api/docs/models/gpt-6-astra. Requests above 272K input tokens use 2x input/cache and 1.5x output rates; computeRowCost applies this only to observed long-context request subsets. Cloud aggregates use Standard short-context estimates because they do not retain per-request context or service tier. Reasoning-effort suffixes use the same base rates."
57
+ },
50
58
  "gpt-5.6-sol": {
51
59
  "input": 4,
52
60
  "output": 20,
@@ -413,7 +421,76 @@
413
421
  "alias": {
414
422
  "auto": "composer-1"
415
423
  },
424
+ "source_exact": {
425
+ "acode": {
426
+ "xopglm53": { "input": 1.11, "output": 3.89, "cache_read": 0.28, "cache_write": 1.11 },
427
+ "xopdeepseekv4pro0813": { "input": 1.25, "output": 3.75, "cache_read": 0.04, "cache_write": 1.25 },
428
+ "xopdeepseekv4flash0731": { "input": 0.14, "output": 0.28, "cache_read": 0.03, "cache_write": 0.14 },
429
+ "xopkimik27code": { "input": 0.90, "output": 3.75, "cache_read": 0.90, "cache_write": 0.90 },
430
+ "xopglm52": { "input": 1.11, "output": 3.89, "cache_read": 0.28, "cache_write": 1.11 },
431
+ "xopdeepseekv4flash": { "input": 0.14, "output": 0.28, "cache_read": 0.03, "cache_write": 0.14 },
432
+ "xopkimik26": { "input": 0.90, "output": 3.75, "cache_read": 0.18, "cache_write": 0.90 },
433
+ "xopdeepseekv4pro": { "input": 1.67, "output": 3.33, "cache_read": 0.14, "cache_write": 1.67 },
434
+ "xopqwen36v35b": { "input": 0.15, "output": 0.90, "cache_read": 0.15, "cache_write": 0.15 },
435
+ "xophunyuan7bmt": { "input": 0.07, "output": 0.28, "cache_read": 0.07, "cache_write": 0.07 },
436
+ "xoppaddleocrv16": { "input": 0.00, "output": 0.00, "cache_read": 0.00, "cache_write": 0.00 },
437
+ "xsparkx2flash": { "input": 0.14, "output": 0.28, "cache_read": 0.14, "cache_write": 0.14 },
438
+ "xopglm51": { "input": 1.11, "output": 3.89, "cache_read": 0.22, "cache_write": 1.11 },
439
+ "xsparkx2": { "input": 0.42, "output": 0.42, "cache_read": 0.42, "cache_write": 0.42 },
440
+ "xop35qwen2b": { "input": 0.03, "output": 0.06, "cache_read": 0.03, "cache_write": 0.03 },
441
+ "xopqwen35397b": { "input": 0.17, "output": 1.00, "cache_read": 0.17, "cache_write": 0.17 },
442
+ "xminimaxm25": { "input": 0.29, "output": 1.17, "cache_read": 0.29, "cache_write": 0.29 },
443
+ "xopglm5": { "input": 0.83, "output": 3.06, "cache_read": 0.17, "cache_write": 0.83 },
444
+ "xopkimik25": { "input": 0.56, "output": 2.92, "cache_read": 0.56, "cache_write": 0.56 },
445
+ "xopdeepseekv32": { "input": 0.14, "output": 0.21, "cache_read": 0.14, "cache_write": 0.14 },
446
+ "xop3qwencodernext": { "input": 0.35, "output": 1.39, "cache_read": 0.35, "cache_write": 0.35 },
447
+ "xopglmv47flash": { "input": 0.14, "output": 0.21, "cache_read": 0.14, "cache_write": 0.14 },
448
+ "xopglm47blth2": { "input": 0.56, "output": 2.22, "cache_read": 0.56, "cache_write": 0.56 },
449
+ "xop3qwen32bvl": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
450
+ "xopdeepseekocr": { "input": 0.00, "output": 0.00, "cache_read": 0.00, "cache_write": 0.00 },
451
+ "xophunyuanocr": { "input": 0.00, "output": 0.00, "cache_read": 0.00, "cache_write": 0.00 },
452
+ "xop3qwen80bnext": { "input": 0.08, "output": 0.33, "cache_read": 0.08, "cache_write": 0.08 },
453
+ "xop3qwen235b2507": { "input": 0.17, "output": 1.67, "cache_read": 0.17, "cache_write": 0.17 },
454
+ "xop3qwen30b2507": { "input": 0.06, "output": 0.63, "cache_read": 0.06, "cache_write": 0.06 },
455
+ "xop3qwen235b": { "input": 0.17, "output": 1.67, "cache_read": 0.17, "cache_write": 0.17 },
456
+ "xop3qwen30b": { "input": 0.06, "output": 0.63, "cache_read": 0.06, "cache_write": 0.06 },
457
+ "xop3qwen32b": { "input": 0.17, "output": 1.67, "cache_read": 0.17, "cache_write": 0.17 },
458
+ "xdeepseekv3": { "input": 0.22, "output": 0.89, "cache_read": 0.22, "cache_write": 0.22 },
459
+ "xdeepseekr1": { "input": 0.44, "output": 1.78, "cache_read": 0.44, "cache_write": 0.44 },
460
+ "xdeepseekr1qwen32b": { "input": 0.22, "output": 0.67, "cache_read": 0.22, "cache_write": 0.22 },
461
+ "xopkimik2blth": { "input": 0.56, "output": 2.22, "cache_read": 0.56, "cache_write": 0.56 },
462
+ "xopkimik2blins": { "input": 0.56, "output": 2.22, "cache_read": 0.56, "cache_write": 0.56 },
463
+ "xop3qwen8breranker": { "input": 0.00, "output": 0.00, "cache_read": 0.00, "cache_write": 0.00 },
464
+ "xop3qwen8bembedding": { "input": 0.00, "output": 0.00, "cache_read": 0.00, "cache_write": 0.00 },
465
+ "xop3qwen0b6": { "input": 0.04, "output": 0.42, "cache_read": 0.04, "cache_write": 0.04 },
466
+ "xop3qwen4b": { "input": 0.04, "output": 0.42, "cache_read": 0.04, "cache_write": 0.04 },
467
+ "xqwen257bchat": { "input": 0.07, "output": 0.14, "cache_read": 0.07, "cache_write": 0.07 },
468
+ "xop3qwen14b": { "input": 0.14, "output": 1.39, "cache_read": 0.14, "cache_write": 0.14 },
469
+ "xop3qwen8b": { "input": 0.07, "output": 0.69, "cache_read": 0.07, "cache_write": 0.07 },
470
+ "xsparkprox": { "input": 1.11, "output": 5.56, "cache_read": 1.11, "cache_write": 1.11 },
471
+ "xspark13b6k": { "input": 0.28, "output": 0.83, "cache_read": 0.28, "cache_write": 0.28 },
472
+ "spark mini": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
473
+ "spark mini instruct": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
474
+ "spark tiny": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
475
+ "internlm2.5_7b_chat": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
476
+ "internlm2.5_1.8b_chat": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
477
+ "qwen_v2.5_7b_base": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
478
+ "xsqwen2d53b": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
479
+ "qwen_v2.5_3b_base": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
480
+ "qwen_v2.5_1.5b_instruct": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
481
+ "qwen_v2.5_1.5b_base": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
482
+ "qwen_v2.5_0.5b_instruct": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
483
+ "qwen_v2.5_0.5b_base": { "input": 0.28, "output": 1.11, "cache_read": 0.28, "cache_write": 0.28 },
484
+ "xsqwenv2s1b5c": { "input": 0.14, "output": 0.28, "cache_read": 0.14, "cache_write": 0.14 },
485
+ "xsqwenv2s0b5c": { "input": 0.28, "output": 0.56, "cache_read": 0.28, "cache_write": 0.28 },
486
+ "xqwen14bchat": { "input": 0.28, "output": 0.83, "cache_read": 0.28, "cache_write": 0.28 }
487
+ }
488
+ },
416
489
  "fuzzy": [
490
+ {
491
+ "match": "gpt-6-astra",
492
+ "ref": "gpt-6-astra"
493
+ },
417
494
  {
418
495
  "match": "gpt-5.6-sol",
419
496
  "ref": "gpt-5.6-sol"
@@ -25,10 +25,9 @@ const PI_SUBSCRIPTION_SOURCES = new Set([
25
25
  // server (including LM Link). Secure Cloud usage has a separate billing path
26
26
  // and is not present in these logs.
27
27
  const LOCAL_INFERENCE_SOURCES = new Set(["lmstudio"]);
28
- // Forward-looking: unreachable for Codex today. Every observed token_count
29
- // reports model_context_window = 258400, below this threshold, so no request
30
- // can exceed it (max raw input seen locally: 238853). The long-context
31
- // repricing stays in place for when OpenAI ships a larger Codex window.
28
+ // OpenAI long-context pricing depends on a single request's raw input,
29
+ // including cached tokens, never a session/day aggregate. Astra supports
30
+ // a larger context window; only observed request subsets receive the premium.
32
31
  const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272_000;
33
32
  const SOURCES_WITH_AUTHORITATIVE_COST = new Set(["grok"]);
34
33
  const SEED_SNAPSHOT_PATH = path.resolve(__dirname, "seed-snapshot.json");
@@ -36,7 +35,6 @@ const DEEPSEEK_TIME_PRICED_MODELS = [
36
35
  "deepseek-v4-flash",
37
36
  "deepseek-v4-pro",
38
37
  ];
39
-
40
38
  // Sync seed load. Done at require-time so callers that haven't awaited
41
39
  // ensurePricingLoaded() (e.g. tests, vite mock startup, edge functions) still
42
40
  // get LiteLLM-backed pricing instead of all-zero. ensurePricingLoaded() will
@@ -164,6 +162,9 @@ function isDeepSeekOffPeak(row) {
164
162
 
165
163
  function getRowPricing(row) {
166
164
  const pricing = getModelPricing(row?.model, { source: row?.source });
165
+ // AStudio uses iFlytek MaaS fixed prices and does not inherit DeepSeek public API
166
+ // time-based discounts.
167
+ if (String(row?.source || "").toLowerCase() === "acode") return pricing;
167
168
  if (!isDeepSeekTimePricedModel(row?.model) || !isDeepSeekOffPeak(row)) return pricing;
168
169
  return {
169
170
  ...pricing,
@@ -193,7 +194,8 @@ function computeRowCost(row) {
193
194
  reportedCost > 0
194
195
  ) return reportedCost;
195
196
  const pricing = getRowPricing(row);
196
- const reasoningIncludedInOutput = row.source === "codex" || row.source === "every-code";
197
+ const reasoningIncludedInOutput =
198
+ row.source === "codex" || row.source === "acode" || row.source === "every-code";
197
199
  const reasoningCost = reasoningIncludedInOutput
198
200
  ? 0
199
201
  : (row.reasoning_output_tokens || 0) * (pricing.output || 0);
@@ -207,9 +209,9 @@ function computeRowCost(row) {
207
209
  );
208
210
 
209
211
  const model = String(row?.model || "").toLowerCase();
210
- const usesGpt56SolLongContextTier =
211
- model === "gpt-5.6" || model.includes("gpt-5.6-sol");
212
- if (!usesGpt56SolLongContextTier) return baseCost;
212
+ const usesOpenAILongContextTier =
213
+ model === "gpt-5.6" || model.includes("gpt-5.6-sol") || model.includes("gpt-6-astra");
214
+ if (!usesOpenAILongContextTier) return baseCost;
213
215
 
214
216
  const bounded = (value, total) => Math.min(
215
217
  Math.max(0, Number(value) || 0),
@@ -1,6 +1,7 @@
1
1
  // Pure pricing-lookup logic. No I/O, no async. Tested in isolation.
2
2
  //
3
3
  // Resolve order:
4
+ // 0. CURATED source exact match (source-specific pricing)
4
5
  // 1. CURATED exact match (self-defined aliases like kiro-*, hy3-*)
5
6
  // 2. LiteLLM exact match (mainstream claude/gpt-5/gemini)
6
7
  // 3. CURATED alias (e.g. "auto" -> "composer-1")
@@ -164,6 +165,15 @@ function normalizeWorkbuddyModel(model) {
164
165
  return model;
165
166
  }
166
167
 
168
+ function normalizeIFlytekMaasModel(model) {
169
+ if (!model || typeof model !== "string") return model;
170
+ const trimmed = model.trim();
171
+ const lower = trimmed.toLowerCase();
172
+ // The default Agent slug maps to the Spark X2 service in the supplied price list.
173
+ if (lower === "xsparkx2agent") return "xsparkx2";
174
+ return trimmed;
175
+ }
176
+
167
177
  // Unsloth Studio can mix local engines and paid API providers in one database.
168
178
  // The parser qualifies metered models with their provider and marks local,
169
179
  // subscription-backed, or ambiguous custom routes as unpriced. Returning a
@@ -182,6 +192,7 @@ function normalizeUnslothModel(model) {
182
192
  // raw model name is preserved for storage/display). Add a source here when its
183
193
  // model strings don't match the LiteLLM/curated keys verbatim.
184
194
  const SOURCE_MODEL_NORMALIZERS = {
195
+ acode: normalizeIFlytekMaasModel,
185
196
  antigravity: normalizeAntigravityModel,
186
197
  claude: normalizeClaudeModel,
187
198
  cursor: normalizeCursorModel,
@@ -242,6 +253,22 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
242
253
  const lower = lookupModel.toLowerCase();
243
254
  const dotForm = buildDotRestoredModel(lookupModel);
244
255
 
256
+ // 0. CURATED source exact. Source-specific prices apply only to their source,
257
+ // preventing collisions with public prices for same-named models from other CLIs.
258
+ const sourceKey = typeof source === "string" ? source.toLowerCase() : "";
259
+ // AStudio does not disclose its routed model. Stop before generic aliases
260
+ // and fuzzy matching can turn an unresolved router into a priced model.
261
+ if (sourceKey === "acode" && (lower === "auto" || lower.endsWith("-auto"))) {
262
+ return { hit: false, source: "miss", value: null };
263
+ }
264
+ const sourceExact = curated.source_exact?.[sourceKey];
265
+ if (sourceExact) {
266
+ const sourceValue = lookupExactCaseInsensitive(sourceExact, lookupModel);
267
+ if (sourceValue) {
268
+ return { hit: true, source: "curated:source-exact", value: sourceValue };
269
+ }
270
+ }
271
+
245
272
  // 1. CURATED exact
246
273
  if (curated.exact && curated.exact[lookupModel]) {
247
274
  return { hit: true, source: "curated:exact", value: curated.exact[lookupModel] };
@@ -382,6 +409,7 @@ module.exports = {
382
409
  lookupPricing,
383
410
  stripReasoningSuffix,
384
411
  normalizeAntigravityModel,
412
+ normalizeIFlytekMaasModel,
385
413
  normalizeClaudeModel,
386
414
  normalizeCursorModel,
387
415
  normalizeZedModel,