tokentracker-cli 0.21.0 → 0.21.1
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.
- package/dashboard/dist/assets/{Card-jA08WeEw.js → Card-Dqh7NgC7.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-chDVOYmG.js → DashboardPage-DvOSzWq2.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DqSYXuUL.js → FadeIn-Ba4HDg1-.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-C11rWv0B.js → HeaderGithubStar-jb65AnnV.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CkEZ9yLK.js → IpCheckPage-BopoeRF9.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BgckTHRQ.js → LandingPage-xBEC-7TO.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BCNW7UWp.js → LeaderboardPage-BChhkcv2.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-BLATxMt-.js → LeaderboardProfilePage-BJ6rhisC.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-arF--WgR.js → LimitsPage-BfN5T40h.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DpoFP0va.js → LoginPage-BPTGJynP.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-kdgc2H6C.js → PopoverPopup-DhX9NeCF.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DV5r9qqP.js → ProviderIcon-CrjWjnUl.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-Bb22ORmU.js → SettingsPage-Bk7duO5H.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-xhtBqVKC.js → SkillsPage-Csd3kUk8.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CUoSVDET.js → WidgetsPage-t4SzNlBi.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-DYb2EChD.js → chevron-down-D6dxVls1.js} +1 -1
- package/dashboard/dist/assets/{download-C-_8o6dh.js → download-qtsjFNo5.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-BgzBlYo7.js → leaderboard-columns-BejzbNvX.js} +1 -1
- package/dashboard/dist/assets/{main-11hApDak.js → main-Bvv7-8LC.js} +2 -2
- package/dashboard/dist/assets/main-CnN_a-Nl.css +1 -0
- package/dashboard/dist/assets/{use-limits-display-prefs-BeGKWUuk.js → use-limits-display-prefs-qSJ1Ys0_.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-nTTHktn0.js → use-native-settings-ClfTqea2.js} +1 -1
- package/dashboard/dist/assets/{use-reduced-motion-DU8Gm6j1.js → use-reduced-motion-DOaOYeS3.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DTPmEB8Y.js → use-usage-limits-CeShSkoj.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +75 -16
- package/dashboard/dist/assets/main-CITVpx5B.css +0 -1
- package/dashboard/dist/brand-logos/every-code.svg +0 -1
- package/dashboard/dist/brand-logos/kilo-code.svg +0 -1
- package/dashboard/dist/brand-logos/oh-my-pi.svg +0 -1
- package/dashboard/dist/wrapped-2025.html +0 -60
- package/src/commands/activate-if-needed.js +0 -41
- package/src/lib/activation-check.js +0 -341
- package/src/lib/opencode-usage-audit.js +0 -205
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
const os = require("node:os");
|
|
2
|
-
const path = require("node:path");
|
|
3
|
-
const fs = require("node:fs/promises");
|
|
4
|
-
const { readJson } = require("./fs");
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 跨 AI CLI 自动激活检测
|
|
8
|
-
* 在 tokentracker 各种命令执行时顺带检测并完成配置
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const AI_CLIS = [
|
|
12
|
-
{
|
|
13
|
-
name: "codex",
|
|
14
|
-
displayName: "Codex",
|
|
15
|
-
checkInstalled: checkCodexInstalled,
|
|
16
|
-
checkConfigured: checkCodexConfigured,
|
|
17
|
-
configure: configureCodex,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: "claude-code",
|
|
21
|
-
displayName: "Claude Code",
|
|
22
|
-
checkInstalled: checkClaudeCodeInstalled,
|
|
23
|
-
checkConfigured: checkClaudeCodeConfigured,
|
|
24
|
-
configure: configureClaudeCode,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "opencode",
|
|
28
|
-
displayName: "OpenCode",
|
|
29
|
-
checkInstalled: checkOpencodeInstalled,
|
|
30
|
-
checkConfigured: checkOpencodeConfigured,
|
|
31
|
-
configure: configureOpencode,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: "every-code",
|
|
35
|
-
displayName: "Every Code",
|
|
36
|
-
checkInstalled: checkEveryCodeInstalled,
|
|
37
|
-
checkConfigured: checkEveryCodeConfigured,
|
|
38
|
-
configure: configureEveryCode,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: "openclaw",
|
|
42
|
-
displayName: "OpenClaw",
|
|
43
|
-
checkInstalled: checkOpenclawInstalled,
|
|
44
|
-
checkConfigured: checkOpenclawConfigured,
|
|
45
|
-
configure: configureOpenclaw,
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 检测所有 AI CLI 并自动配置未完成的
|
|
51
|
-
* @param {Object} options
|
|
52
|
-
* @param {string} options.home - home目录
|
|
53
|
-
* @param {boolean} options.silent - 是否静默模式
|
|
54
|
-
* @param {boolean} options.autoConfigure - 是否自动配置(否则仅提示)
|
|
55
|
-
*/
|
|
56
|
-
async function checkAndActivate({ home = os.homedir(), silent = true, autoConfigure = true } = {}) {
|
|
57
|
-
const results = [];
|
|
58
|
-
|
|
59
|
-
for (const cli of AI_CLIS) {
|
|
60
|
-
try {
|
|
61
|
-
const isInstalled = await cli.checkInstalled({ home });
|
|
62
|
-
if (!isInstalled) continue;
|
|
63
|
-
|
|
64
|
-
const isConfigured = await cli.checkConfigured({ home });
|
|
65
|
-
if (isConfigured) continue;
|
|
66
|
-
|
|
67
|
-
// 发现已安装但未配置的 CLI
|
|
68
|
-
if (autoConfigure) {
|
|
69
|
-
const success = await cli.configure({ home, silent });
|
|
70
|
-
results.push({
|
|
71
|
-
name: cli.name,
|
|
72
|
-
displayName: cli.displayName,
|
|
73
|
-
action: success ? "configured" : "failed",
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (!silent && success) {
|
|
77
|
-
console.log(`✅ 已自动配置 ${cli.displayName} 集成`);
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
results.push({
|
|
81
|
-
name: cli.name,
|
|
82
|
-
displayName: cli.displayName,
|
|
83
|
-
action: "pending",
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (!silent) {
|
|
87
|
-
console.log(`⏳ 检测到 ${cli.displayName} 未配置,运行 'tokentracker init' 以配置`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch (err) {
|
|
91
|
-
// 静默忽略错误,不影响主流程
|
|
92
|
-
if (!silent) {
|
|
93
|
-
console.error(`检查 ${cli.displayName} 失败:`, err.message);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return results;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ===== Codex 检测与配置 =====
|
|
102
|
-
|
|
103
|
-
async function checkCodexInstalled({ home }) {
|
|
104
|
-
const configPath = path.join(home, ".codex", "config.toml");
|
|
105
|
-
try {
|
|
106
|
-
await fs.access(configPath);
|
|
107
|
-
return true;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function checkCodexConfigured({ home }) {
|
|
114
|
-
const configPath = path.join(home, ".codex", "config.toml");
|
|
115
|
-
try {
|
|
116
|
-
const content = await fs.readFile(configPath, "utf8");
|
|
117
|
-
// 检查是否已配置 notify
|
|
118
|
-
return content.includes("tokentracker") || content.includes("notify");
|
|
119
|
-
} catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function configureCodex({ home, silent }) {
|
|
125
|
-
try {
|
|
126
|
-
// 使用现有的 codex-config 模块
|
|
127
|
-
const { upsertCodexNotify } = require("./codex-config");
|
|
128
|
-
const notifyCmd = path.join(home, ".tokentracker", "bin", "notify.cjs");
|
|
129
|
-
const codexConfigPath = path.join(home, ".codex", "config.toml");
|
|
130
|
-
const notifyOriginalPath = path.join(home, ".tokentracker", "backups", "codex-notify-original.json");
|
|
131
|
-
|
|
132
|
-
await upsertCodexNotify({
|
|
133
|
-
codexConfigPath,
|
|
134
|
-
notifyCmd,
|
|
135
|
-
notifyOriginalPath,
|
|
136
|
-
});
|
|
137
|
-
return true;
|
|
138
|
-
} catch (err) {
|
|
139
|
-
if (!silent) console.error("配置 Codex 失败:", err.message);
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ===== Claude Code 检测与配置 =====
|
|
145
|
-
|
|
146
|
-
async function checkClaudeCodeInstalled({ home }) {
|
|
147
|
-
const settingsPath = path.join(home, ".claude", "settings.json");
|
|
148
|
-
try {
|
|
149
|
-
await fs.access(settingsPath);
|
|
150
|
-
return true;
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function checkClaudeCodeConfigured({ home }) {
|
|
157
|
-
const settingsPath = path.join(home, ".claude", "settings.json");
|
|
158
|
-
try {
|
|
159
|
-
const settings = await readJson(settingsPath);
|
|
160
|
-
// 检查是否已有 tokentracker 相关的 hook
|
|
161
|
-
const hooks = settings?.hooks?.SessionStart || [];
|
|
162
|
-
return hooks.some(h =>
|
|
163
|
-
h.hooks?.some(hook => hook.command?.includes("tokentracker"))
|
|
164
|
-
);
|
|
165
|
-
} catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function configureClaudeCode({ home, silent }) {
|
|
171
|
-
try {
|
|
172
|
-
const settingsPath = path.join(home, ".claude", "settings.json");
|
|
173
|
-
const settings = (await readJson(settingsPath)) || {};
|
|
174
|
-
|
|
175
|
-
// 添加 SessionStart hook
|
|
176
|
-
if (!settings.hooks) settings.hooks = {};
|
|
177
|
-
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
178
|
-
|
|
179
|
-
// 检查是否已存在
|
|
180
|
-
const exists = settings.hooks.SessionStart.some(h =>
|
|
181
|
-
h.matcher === "startup" &&
|
|
182
|
-
h.hooks?.some(hook => hook.command?.includes("tokentracker activate-if-needed"))
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
if (!exists) {
|
|
186
|
-
settings.hooks.SessionStart.push({
|
|
187
|
-
matcher: "startup",
|
|
188
|
-
hooks: [{
|
|
189
|
-
type: "command",
|
|
190
|
-
command: "tokentracker activate-if-needed --silent 2>/dev/null || true"
|
|
191
|
-
}]
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
195
|
-
}
|
|
196
|
-
return true;
|
|
197
|
-
} catch (err) {
|
|
198
|
-
if (!silent) console.error("配置 Claude Code 失败:", err.message);
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ===== OpenCode 检测与配置 =====
|
|
204
|
-
|
|
205
|
-
async function checkOpencodeInstalled({ home }) {
|
|
206
|
-
const configPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
207
|
-
try {
|
|
208
|
-
await fs.access(configPath);
|
|
209
|
-
return true;
|
|
210
|
-
} catch {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function checkOpencodeConfigured({ home }) {
|
|
216
|
-
const pluginDir = path.join(home, ".config", "opencode", "plugins");
|
|
217
|
-
try {
|
|
218
|
-
const files = await fs.readdir(pluginDir);
|
|
219
|
-
return files.some(f => f.includes("tokentracker"));
|
|
220
|
-
} catch {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function configureOpencode({ home, silent }) {
|
|
226
|
-
try {
|
|
227
|
-
const pluginDir = path.join(home, ".config", "opencode", "plugins");
|
|
228
|
-
await fs.mkdir(pluginDir, { recursive: true });
|
|
229
|
-
|
|
230
|
-
const pluginPath = path.join(pluginDir, "tokentracker-activation.js");
|
|
231
|
-
const pluginCode = `export const TokentrackerActivation = async ({ $ }) => {
|
|
232
|
-
return {
|
|
233
|
-
"session.created": async () => {
|
|
234
|
-
await $'tokentracker activate-if-needed --silent'.quiet().nothrow();
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
};`;
|
|
238
|
-
|
|
239
|
-
await fs.writeFile(pluginPath, pluginCode, "utf8");
|
|
240
|
-
return true;
|
|
241
|
-
} catch (err) {
|
|
242
|
-
if (!silent) console.error("配置 OpenCode 失败:", err.message);
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ===== Every Code 检测与配置 =====
|
|
248
|
-
|
|
249
|
-
async function checkEveryCodeInstalled({ home }) {
|
|
250
|
-
const configPath = path.join(home, ".code", "config.toml");
|
|
251
|
-
try {
|
|
252
|
-
await fs.access(configPath);
|
|
253
|
-
return true;
|
|
254
|
-
} catch {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function checkEveryCodeConfigured({ home }) {
|
|
260
|
-
const configPath = path.join(home, ".code", "config.toml");
|
|
261
|
-
try {
|
|
262
|
-
const content = await fs.readFile(configPath, "utf8");
|
|
263
|
-
return content.includes("tokentracker");
|
|
264
|
-
} catch {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async function configureEveryCode({ home, silent }) {
|
|
270
|
-
try {
|
|
271
|
-
// Every Code 配置类似 Codex
|
|
272
|
-
const configPath = path.join(home, ".code", "config.toml");
|
|
273
|
-
let content = "";
|
|
274
|
-
try {
|
|
275
|
-
content = await fs.readFile(configPath, "utf8");
|
|
276
|
-
} catch {
|
|
277
|
-
content = "";
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const notifyCmd = path.join(home, ".tokentracker", "bin", "notify.cjs");
|
|
281
|
-
const notifyLine = `notify = ["/usr/bin/env", "node", "${notifyCmd}"]`;
|
|
282
|
-
|
|
283
|
-
if (!content.includes("tokentracker")) {
|
|
284
|
-
content = content.trim() + "\n\n# tokentracker integration\n" + notifyLine + "\n";
|
|
285
|
-
await fs.writeFile(configPath, content, "utf8");
|
|
286
|
-
}
|
|
287
|
-
return true;
|
|
288
|
-
} catch (err) {
|
|
289
|
-
if (!silent) console.error("配置 Every Code 失败:", err.message);
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
module.exports = {
|
|
295
|
-
checkAndActivate,
|
|
296
|
-
AI_CLIS,
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// ===== OpenClaw 检测与配置 =====
|
|
300
|
-
|
|
301
|
-
async function checkOpenclawInstalled({ home }) {
|
|
302
|
-
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
303
|
-
try {
|
|
304
|
-
await fs.access(configPath);
|
|
305
|
-
return true;
|
|
306
|
-
} catch {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async function checkOpenclawConfigured({ home }) {
|
|
312
|
-
const { probeOpenclawSessionPluginState } = require("./openclaw-session-plugin");
|
|
313
|
-
const { resolveTrackerPaths } = require("./tracker-paths");
|
|
314
|
-
try {
|
|
315
|
-
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
316
|
-
const state = await probeOpenclawSessionPluginState({ home, trackerDir, env: process.env });
|
|
317
|
-
return state?.configured === true;
|
|
318
|
-
} catch {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async function configureOpenclaw({ home, silent }) {
|
|
324
|
-
try {
|
|
325
|
-
const { installOpenclawSessionPlugin } = require("./openclaw-session-plugin");
|
|
326
|
-
const { resolveTrackerPaths } = require("./tracker-paths");
|
|
327
|
-
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
328
|
-
|
|
329
|
-
const result = await installOpenclawSessionPlugin({
|
|
330
|
-
home,
|
|
331
|
-
trackerDir,
|
|
332
|
-
packageName: "tokentracker-cli",
|
|
333
|
-
env: process.env,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
return result?.configured === true;
|
|
337
|
-
} catch (err) {
|
|
338
|
-
if (!silent) console.error("配置 OpenClaw 失败:", err.message);
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
const fs = require("node:fs/promises");
|
|
2
|
-
const os = require("node:os");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
|
|
5
|
-
const { listOpencodeMessageFiles, parseOpencodeIncremental } = require("./rollout");
|
|
6
|
-
|
|
7
|
-
const BUCKET_SEPARATOR = "|";
|
|
8
|
-
const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9
|
-
|
|
10
|
-
function formatHourKey(date) {
|
|
11
|
-
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(
|
|
12
|
-
date.getUTCDate(),
|
|
13
|
-
).padStart(2, "0")}T${String(date.getUTCHours()).padStart(2, "0")}:${String(
|
|
14
|
-
date.getUTCMinutes() >= 30 ? 30 : 0,
|
|
15
|
-
).padStart(2, "0")}:00`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function toBig(value) {
|
|
19
|
-
if (typeof value === "bigint") return value;
|
|
20
|
-
if (typeof value === "number") return BigInt(value);
|
|
21
|
-
if (typeof value === "string" && value.trim()) return BigInt(value);
|
|
22
|
-
return 0n;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function addTotals(target, delta) {
|
|
26
|
-
target.input_tokens += toBig(delta.input_tokens);
|
|
27
|
-
target.cached_input_tokens += toBig(delta.cached_input_tokens);
|
|
28
|
-
target.output_tokens += toBig(delta.output_tokens);
|
|
29
|
-
target.reasoning_output_tokens += toBig(delta.reasoning_output_tokens);
|
|
30
|
-
target.total_tokens += toBig(delta.total_tokens);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function buildLocalHourlyTotals({ storageDir, source = "opencode" }) {
|
|
34
|
-
const messageFiles = await listOpencodeMessageFiles(storageDir);
|
|
35
|
-
const queuePath = path.join(
|
|
36
|
-
os.tmpdir(),
|
|
37
|
-
`tokentracker-opencode-audit-${process.pid}-${Date.now()}.jsonl`,
|
|
38
|
-
);
|
|
39
|
-
const cursors = { version: 1, files: {}, hourly: null, opencode: null };
|
|
40
|
-
|
|
41
|
-
await parseOpencodeIncremental({ messageFiles, cursors, queuePath, source });
|
|
42
|
-
await fs.rm(queuePath, { force: true }).catch(() => {});
|
|
43
|
-
|
|
44
|
-
const byHour = new Map();
|
|
45
|
-
let minDay = null;
|
|
46
|
-
let maxDay = null;
|
|
47
|
-
|
|
48
|
-
for (const [key, bucket] of Object.entries(cursors.hourly?.buckets || {})) {
|
|
49
|
-
const [bucketSource, , hourStart] = String(key).split(BUCKET_SEPARATOR);
|
|
50
|
-
if (bucketSource !== source || !hourStart) continue;
|
|
51
|
-
const dt = new Date(hourStart);
|
|
52
|
-
if (!Number.isFinite(dt.getTime())) continue;
|
|
53
|
-
const hourKey = formatHourKey(dt);
|
|
54
|
-
const dayKey = hourKey.slice(0, 10);
|
|
55
|
-
|
|
56
|
-
if (!minDay || dayKey < minDay) minDay = dayKey;
|
|
57
|
-
if (!maxDay || dayKey > maxDay) maxDay = dayKey;
|
|
58
|
-
|
|
59
|
-
const totals = byHour.get(hourKey) || {
|
|
60
|
-
input_tokens: 0n,
|
|
61
|
-
cached_input_tokens: 0n,
|
|
62
|
-
output_tokens: 0n,
|
|
63
|
-
reasoning_output_tokens: 0n,
|
|
64
|
-
total_tokens: 0n,
|
|
65
|
-
};
|
|
66
|
-
addTotals(totals, bucket.totals || {});
|
|
67
|
-
byHour.set(hourKey, totals);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { byHour, minDay, maxDay };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizeServerRows(rows) {
|
|
74
|
-
const map = new Map();
|
|
75
|
-
for (const row of rows || []) {
|
|
76
|
-
if (!row || !row.hour) continue;
|
|
77
|
-
map.set(row.hour, {
|
|
78
|
-
missing: Boolean(row.missing),
|
|
79
|
-
totals: {
|
|
80
|
-
input_tokens: toBig(row.input_tokens),
|
|
81
|
-
cached_input_tokens: toBig(row.cached_input_tokens),
|
|
82
|
-
output_tokens: toBig(row.output_tokens),
|
|
83
|
-
reasoning_output_tokens: toBig(row.reasoning_output_tokens),
|
|
84
|
-
total_tokens: toBig(row.total_tokens),
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
return map;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function diffTotals(local, server) {
|
|
92
|
-
return {
|
|
93
|
-
input_tokens: local.input_tokens - server.input_tokens,
|
|
94
|
-
cached_input_tokens: local.cached_input_tokens - server.cached_input_tokens,
|
|
95
|
-
output_tokens: local.output_tokens - server.output_tokens,
|
|
96
|
-
reasoning_output_tokens: local.reasoning_output_tokens - server.reasoning_output_tokens,
|
|
97
|
-
total_tokens: local.total_tokens - server.total_tokens,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function maxAbsDelta(delta) {
|
|
102
|
-
return [
|
|
103
|
-
delta.input_tokens,
|
|
104
|
-
delta.cached_input_tokens,
|
|
105
|
-
delta.output_tokens,
|
|
106
|
-
delta.reasoning_output_tokens,
|
|
107
|
-
delta.total_tokens,
|
|
108
|
-
].reduce((acc, value) => {
|
|
109
|
-
const abs = value < 0n ? -value : value;
|
|
110
|
-
return abs > acc ? abs : acc;
|
|
111
|
-
}, 0n);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function isValidDay(value) {
|
|
115
|
-
if (!DAY_RE.test(value)) return false;
|
|
116
|
-
const dt = new Date(`${value}T00:00:00.000Z`);
|
|
117
|
-
return Number.isFinite(dt.getTime());
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function listDays(from, to) {
|
|
121
|
-
if (!isValidDay(from) || !isValidDay(to)) return [];
|
|
122
|
-
if (from > to) return [];
|
|
123
|
-
const out = [];
|
|
124
|
-
const start = new Date(`${from}T00:00:00.000Z`);
|
|
125
|
-
const end = new Date(`${to}T00:00:00.000Z`);
|
|
126
|
-
for (let dt = start; dt <= end; dt = new Date(dt.getTime() + 24 * 60 * 60 * 1000)) {
|
|
127
|
-
const day = `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, "0")}-${String(
|
|
128
|
-
dt.getUTCDate(),
|
|
129
|
-
).padStart(2, "0")}`;
|
|
130
|
-
out.push(day);
|
|
131
|
-
}
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function auditOpencodeUsage({ storageDir, from, to, fetchHourly, includeMissing = false }) {
|
|
136
|
-
const local = await buildLocalHourlyTotals({ storageDir, source: "opencode" });
|
|
137
|
-
if (!local.minDay || !local.maxDay) {
|
|
138
|
-
throw new Error("No local opencode data found");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const fromDay = from || local.minDay;
|
|
142
|
-
const toDay = to || local.maxDay;
|
|
143
|
-
const days = listDays(fromDay, toDay);
|
|
144
|
-
if (days.length === 0) {
|
|
145
|
-
throw new Error("Invalid date range for audit");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const diffs = [];
|
|
149
|
-
let matched = 0;
|
|
150
|
-
let mismatched = 0;
|
|
151
|
-
let incomplete = 0;
|
|
152
|
-
let maxDelta = 0n;
|
|
153
|
-
|
|
154
|
-
for (const day of days) {
|
|
155
|
-
const server = await fetchHourly(day);
|
|
156
|
-
const serverByHour = normalizeServerRows(server?.data || []);
|
|
157
|
-
for (let h = 0; h < 24; h++) {
|
|
158
|
-
for (const m of [0, 30]) {
|
|
159
|
-
const hourKey = `${day}T${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:00`;
|
|
160
|
-
const localTotals = local.byHour.get(hourKey) || {
|
|
161
|
-
input_tokens: 0n,
|
|
162
|
-
cached_input_tokens: 0n,
|
|
163
|
-
output_tokens: 0n,
|
|
164
|
-
reasoning_output_tokens: 0n,
|
|
165
|
-
total_tokens: 0n,
|
|
166
|
-
};
|
|
167
|
-
const serverEntry = serverByHour.get(hourKey) || null;
|
|
168
|
-
if (serverEntry?.missing && !includeMissing) {
|
|
169
|
-
incomplete += 1;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
const serverTotals = serverEntry?.totals || {
|
|
173
|
-
input_tokens: 0n,
|
|
174
|
-
cached_input_tokens: 0n,
|
|
175
|
-
output_tokens: 0n,
|
|
176
|
-
reasoning_output_tokens: 0n,
|
|
177
|
-
total_tokens: 0n,
|
|
178
|
-
};
|
|
179
|
-
const delta = diffTotals(localTotals, serverTotals);
|
|
180
|
-
const deltaMax = maxAbsDelta(delta);
|
|
181
|
-
if (deltaMax === 0n) {
|
|
182
|
-
matched += 1;
|
|
183
|
-
} else {
|
|
184
|
-
mismatched += 1;
|
|
185
|
-
if (deltaMax > maxDelta) maxDelta = deltaMax;
|
|
186
|
-
diffs.push({ hour: hourKey, local: localTotals, server: serverTotals, delta });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
summary: {
|
|
194
|
-
days: days.length,
|
|
195
|
-
slots: days.length * 48,
|
|
196
|
-
matched,
|
|
197
|
-
mismatched,
|
|
198
|
-
incomplete,
|
|
199
|
-
maxDelta,
|
|
200
|
-
},
|
|
201
|
-
diffs,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
module.exports = { auditOpencodeUsage, buildLocalHourlyTotals };
|