gcusage 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -15
- package/dist/core/aggregate.js +170 -0
- package/dist/core/commands/trim.js +63 -0
- package/dist/core/logs/index.js +43 -0
- package/dist/core/logs/split.js +50 -0
- package/dist/core/metrics/index.js +263 -0
- package/dist/core/range.js +57 -0
- package/dist/core/render/table.js +192 -0
- package/dist/core/run.js +65 -0
- package/dist/core/utils/format.js +45 -0
- package/dist/core/utils/period.js +8 -0
- package/dist/core/utils/time.js +69 -0
- package/dist/index.js +25 -926
- package/dist/messages.js +36 -0
- package/dist/types.js +2 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,55 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const os_1 = __importDefault(require("os"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
4
|
const commander_1 = require("commander");
|
|
5
|
+
const messages_1 = require("./messages");
|
|
6
|
+
const run_1 = require("./core/run");
|
|
7
|
+
const trim_1 = require("./core/commands/trim");
|
|
8
|
+
const range_1 = require("./core/range");
|
|
9
|
+
const table_1 = require("./core/render/table");
|
|
10
|
+
const period_1 = require("./core/utils/period");
|
|
11
|
+
const time_1 = require("./core/utils/time");
|
|
11
12
|
const program = new commander_1.Command();
|
|
12
13
|
let didRunSubcommand = false;
|
|
13
14
|
program
|
|
14
|
-
.name(
|
|
15
|
-
.description(
|
|
16
|
-
.option("--since <time>",
|
|
17
|
-
.option("--until <time>",
|
|
18
|
-
.option("--period <period>",
|
|
19
|
-
.option("--model <model>",
|
|
20
|
-
.option("--type <type>",
|
|
21
|
-
.option("--json",
|
|
15
|
+
.name(messages_1.MESSAGES.CLI_NAME)
|
|
16
|
+
.description(messages_1.MESSAGES.CLI_DESC)
|
|
17
|
+
.option("--since <time>", messages_1.MESSAGES.OPTION_SINCE)
|
|
18
|
+
.option("--until <time>", messages_1.MESSAGES.OPTION_UNTIL)
|
|
19
|
+
.option("--period <period>", messages_1.MESSAGES.OPTION_PERIOD, "day")
|
|
20
|
+
.option("--model <model>", messages_1.MESSAGES.OPTION_MODEL)
|
|
21
|
+
.option("--type <type>", messages_1.MESSAGES.OPTION_TYPE)
|
|
22
|
+
.option("--json", messages_1.MESSAGES.OPTION_JSON)
|
|
22
23
|
.action(async (opts) => {
|
|
23
|
-
const periodValue = normalizePeriod(opts.period);
|
|
24
|
+
const periodValue = (0, period_1.normalizePeriod)(opts.period);
|
|
24
25
|
if (!periodValue) {
|
|
25
|
-
console.error(
|
|
26
|
+
console.error(messages_1.MESSAGES.PERIOD_INVALID);
|
|
26
27
|
process.exit(1);
|
|
27
28
|
}
|
|
28
29
|
const period = periodValue;
|
|
29
|
-
const sinceMs = parseTimeSpec(opts.since, "since");
|
|
30
|
-
const untilMs = parseTimeSpec(opts.until, "until");
|
|
31
|
-
const periodProvided = isPeriodProvided(process.argv);
|
|
30
|
+
const sinceMs = (0, time_1.parseTimeSpec)(opts.since, "since");
|
|
31
|
+
const untilMs = (0, time_1.parseTimeSpec)(opts.until, "until");
|
|
32
|
+
const periodProvided = (0, range_1.isPeriodProvided)(process.argv);
|
|
32
33
|
if (sinceMs !== null && untilMs !== null && sinceMs > untilMs) {
|
|
33
|
-
console.error(
|
|
34
|
+
console.error(messages_1.MESSAGES.SINCE_AFTER_UNTIL);
|
|
34
35
|
process.exit(1);
|
|
35
36
|
}
|
|
36
37
|
const modelFilter = typeof opts.model === "string" ? opts.model : null;
|
|
37
38
|
const typeFilter = typeof opts.type === "string" ? opts.type : null;
|
|
38
39
|
const outputJson = Boolean(opts.json);
|
|
39
|
-
const result = await run(period, periodProvided, sinceMs, untilMs, modelFilter, typeFilter);
|
|
40
|
+
const result = await (0, run_1.run)(period, periodProvided, sinceMs, untilMs, modelFilter, typeFilter);
|
|
40
41
|
if (outputJson) {
|
|
41
42
|
process.stdout.write(JSON.stringify(result.json, null, 2));
|
|
42
43
|
process.stdout.write("\n");
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
|
-
renderTable(result, period);
|
|
46
|
+
(0, table_1.renderTable)(result, period);
|
|
46
47
|
});
|
|
47
48
|
program
|
|
48
49
|
.command("trim")
|
|
49
|
-
.description(
|
|
50
|
+
.description(messages_1.MESSAGES.TRIM_DESC)
|
|
50
51
|
.action(async () => {
|
|
51
52
|
didRunSubcommand = true;
|
|
52
|
-
await runTrim();
|
|
53
|
+
await (0, trim_1.runTrim)();
|
|
53
54
|
});
|
|
54
55
|
main().catch((err) => {
|
|
55
56
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -60,905 +61,3 @@ async function main() {
|
|
|
60
61
|
if (didRunSubcommand)
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
|
-
async function run(activePeriod, periodWasProvided, rawSince, rawUntil, modelFilter, typeFilter) {
|
|
64
|
-
const logFiles = await findLogFiles();
|
|
65
|
-
if (logFiles.length === 0) {
|
|
66
|
-
console.error("未找到任何日志文件:~/.gemini/telemetry.log");
|
|
67
|
-
return { json: [], table: [], range: { sinceMs: null, untilMs: null } };
|
|
68
|
-
}
|
|
69
|
-
const points = [];
|
|
70
|
-
for (const file of logFiles) {
|
|
71
|
-
const filePoints = await parseLogFile(file);
|
|
72
|
-
points.push(...filePoints);
|
|
73
|
-
}
|
|
74
|
-
const range = resolveRange(activePeriod, periodWasProvided, rawSince, rawUntil);
|
|
75
|
-
const filteredByType = points.filter((p) => {
|
|
76
|
-
if (modelFilter && p.model !== modelFilter)
|
|
77
|
-
return false;
|
|
78
|
-
if (typeFilter && p.type !== typeFilter)
|
|
79
|
-
return false;
|
|
80
|
-
return true;
|
|
81
|
-
});
|
|
82
|
-
const summaries = buildSessionSummaries(filteredByType).filter((s) => {
|
|
83
|
-
if (range.sinceMs !== null && s.sessionStartMs < range.sinceMs)
|
|
84
|
-
return false;
|
|
85
|
-
if (range.untilMs !== null && s.sessionStartMs > range.untilMs)
|
|
86
|
-
return false;
|
|
87
|
-
return true;
|
|
88
|
-
});
|
|
89
|
-
if (activePeriod === "session") {
|
|
90
|
-
const table = summaries.map((s) => toSessionTotals(s));
|
|
91
|
-
const json = table.map((row) => ({
|
|
92
|
-
date: row.date,
|
|
93
|
-
session: row.sessionId,
|
|
94
|
-
models: Array.from(row.models),
|
|
95
|
-
input: Math.round(row.input),
|
|
96
|
-
output: Math.round(row.output),
|
|
97
|
-
thought: Math.round(row.thought),
|
|
98
|
-
cache: Math.round(row.cache),
|
|
99
|
-
tool: Math.round(row.tool)
|
|
100
|
-
}));
|
|
101
|
-
return { json, table, range };
|
|
102
|
-
}
|
|
103
|
-
const table = aggregateSessionsToDay(summaries);
|
|
104
|
-
const json = table.map((row) => ({
|
|
105
|
-
date: row.date,
|
|
106
|
-
models: Array.from(row.models),
|
|
107
|
-
input: Math.round(row.input),
|
|
108
|
-
output: Math.round(row.output),
|
|
109
|
-
thought: Math.round(row.thought),
|
|
110
|
-
cache: Math.round(row.cache),
|
|
111
|
-
tool: Math.round(row.tool)
|
|
112
|
-
}));
|
|
113
|
-
return { json, table, range };
|
|
114
|
-
}
|
|
115
|
-
async function runTrim() {
|
|
116
|
-
const logPath = path_1.default.join(os_1.default.homedir(), ".gemini", "telemetry.log");
|
|
117
|
-
const backupPath = path_1.default.join(os_1.default.homedir(), ".gemini", "telemetry.log.bak");
|
|
118
|
-
try {
|
|
119
|
-
const stat = await fs_1.default.promises.stat(logPath);
|
|
120
|
-
if (!stat.isFile()) {
|
|
121
|
-
console.error("未找到 telemetry.log");
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
console.error("未找到 telemetry.log");
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
await fs_1.default.promises.copyFile(logPath, backupPath);
|
|
130
|
-
const content = await fs_1.default.promises.readFile(logPath, "utf8");
|
|
131
|
-
const objects = splitJsonObjects(content);
|
|
132
|
-
const lastPoints = new Map();
|
|
133
|
-
for (const objText of objects) {
|
|
134
|
-
let obj;
|
|
135
|
-
try {
|
|
136
|
-
obj = JSON.parse(objText);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const points = extractTokenUsageDataPoints(obj);
|
|
142
|
-
for (const dp of points) {
|
|
143
|
-
const sessionId = readSessionId(dp) || "no-session";
|
|
144
|
-
const model = readAttributeValueFromDataPoint(dp, "model") || "unknown";
|
|
145
|
-
const type = readAttributeValueFromDataPoint(dp, "type") || "unknown";
|
|
146
|
-
const time = readTimestampMs(dp);
|
|
147
|
-
if (time === null)
|
|
148
|
-
continue;
|
|
149
|
-
const key = `${sessionId}||${model}||${type}`;
|
|
150
|
-
const prev = lastPoints.get(key);
|
|
151
|
-
if (!prev || time >= prev.timestampMs) {
|
|
152
|
-
lastPoints.set(key, { timestampMs: time, dataPoint: dp });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const dataPoints = Array.from(lastPoints.values()).map((v) => v.dataPoint);
|
|
157
|
-
const minimalBlock = {
|
|
158
|
-
descriptor: { name: "gemini_cli.token.usage" },
|
|
159
|
-
dataPoints
|
|
160
|
-
};
|
|
161
|
-
const outputText = JSON.stringify(minimalBlock);
|
|
162
|
-
await fs_1.default.promises.writeFile(logPath, outputText, "utf8");
|
|
163
|
-
await fs_1.default.promises.unlink(backupPath);
|
|
164
|
-
console.log("瘦身完成:telemetry.log 已仅保留 token 使用数据");
|
|
165
|
-
}
|
|
166
|
-
async function findLogFiles() {
|
|
167
|
-
const logPath = path_1.default.join(os_1.default.homedir(), ".gemini", "telemetry.log");
|
|
168
|
-
try {
|
|
169
|
-
const stat = await fs_1.default.promises.stat(logPath);
|
|
170
|
-
if (stat.isFile())
|
|
171
|
-
return [logPath];
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return [];
|
|
175
|
-
}
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
async function parseLogFile(filePath) {
|
|
179
|
-
const points = [];
|
|
180
|
-
const content = await fs_1.default.promises.readFile(filePath, "utf8");
|
|
181
|
-
const objects = splitJsonObjects(content);
|
|
182
|
-
for (const objText of objects) {
|
|
183
|
-
let obj;
|
|
184
|
-
try {
|
|
185
|
-
obj = JSON.parse(objText);
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const extracted = extractMetricPoints(obj);
|
|
191
|
-
if (extracted.length > 0) {
|
|
192
|
-
points.push(...extracted);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return points;
|
|
196
|
-
}
|
|
197
|
-
function splitJsonObjects(input) {
|
|
198
|
-
const results = [];
|
|
199
|
-
let depth = 0;
|
|
200
|
-
let inString = false;
|
|
201
|
-
let escapeNext = false;
|
|
202
|
-
let start = -1;
|
|
203
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
204
|
-
const ch = input[i];
|
|
205
|
-
if (inString) {
|
|
206
|
-
if (escapeNext) {
|
|
207
|
-
escapeNext = false;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (ch === "\\\\") {
|
|
211
|
-
escapeNext = true;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (ch === "\"") {
|
|
215
|
-
inString = false;
|
|
216
|
-
}
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (ch === "\"") {
|
|
220
|
-
inString = true;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (ch === "{") {
|
|
224
|
-
if (depth === 0) {
|
|
225
|
-
start = i;
|
|
226
|
-
}
|
|
227
|
-
depth += 1;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (ch === "}") {
|
|
231
|
-
if (depth > 0) {
|
|
232
|
-
depth -= 1;
|
|
233
|
-
if (depth === 0 && start >= 0) {
|
|
234
|
-
const chunk = input.slice(start, i + 1).trim();
|
|
235
|
-
if (chunk)
|
|
236
|
-
results.push(chunk);
|
|
237
|
-
start = -1;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return results;
|
|
243
|
-
}
|
|
244
|
-
function extractMetricPoints(root) {
|
|
245
|
-
const points = [];
|
|
246
|
-
const stack = [root];
|
|
247
|
-
while (stack.length > 0) {
|
|
248
|
-
const node = stack.pop();
|
|
249
|
-
if (!node)
|
|
250
|
-
continue;
|
|
251
|
-
if (Array.isArray(node)) {
|
|
252
|
-
for (const item of node)
|
|
253
|
-
stack.push(item);
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (typeof node !== "object")
|
|
257
|
-
continue;
|
|
258
|
-
const obj = node;
|
|
259
|
-
const name = typeof obj.name === "string" ? obj.name : null;
|
|
260
|
-
const descriptorName = obj.descriptor && typeof obj.descriptor === "object"
|
|
261
|
-
? typeof obj.descriptor.name === "string"
|
|
262
|
-
? obj.descriptor.name
|
|
263
|
-
: null
|
|
264
|
-
: null;
|
|
265
|
-
if (name === "gemini_cli.token.usage" || descriptorName === "gemini_cli.token.usage") {
|
|
266
|
-
const dataPoints = obj.dataPoints;
|
|
267
|
-
if (Array.isArray(dataPoints)) {
|
|
268
|
-
for (const dp of dataPoints) {
|
|
269
|
-
const metric = buildPointFromDataPoint(dp);
|
|
270
|
-
if (metric)
|
|
271
|
-
points.push(metric);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
for (const value of Object.values(obj)) {
|
|
276
|
-
stack.push(value);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return points;
|
|
280
|
-
}
|
|
281
|
-
function buildSessionSummaries(points) {
|
|
282
|
-
const sessionMap = new Map();
|
|
283
|
-
for (const p of points) {
|
|
284
|
-
if (!p.sessionId)
|
|
285
|
-
continue;
|
|
286
|
-
const entry = sessionMap.get(p.sessionId);
|
|
287
|
-
const modelTypeKey = `${p.model}||${p.type}`;
|
|
288
|
-
if (!entry) {
|
|
289
|
-
const map = new Map();
|
|
290
|
-
map.set(modelTypeKey, { timestampMs: p.timestampMs, value: p.value, type: p.type });
|
|
291
|
-
sessionMap.set(p.sessionId, {
|
|
292
|
-
sessionStartMs: p.timestampMs,
|
|
293
|
-
models: new Set([p.model]),
|
|
294
|
-
lastByModelType: map
|
|
295
|
-
});
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
entry.sessionStartMs = Math.min(entry.sessionStartMs, p.timestampMs);
|
|
299
|
-
entry.models.add(p.model);
|
|
300
|
-
const last = entry.lastByModelType.get(modelTypeKey);
|
|
301
|
-
if (!last || p.timestampMs >= last.timestampMs) {
|
|
302
|
-
entry.lastByModelType.set(modelTypeKey, { timestampMs: p.timestampMs, value: p.value, type: p.type });
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const summaries = [];
|
|
306
|
-
for (const [sessionId, entry] of sessionMap.entries()) {
|
|
307
|
-
const totals = { input: 0, output: 0, thought: 0, cache: 0, tool: 0 };
|
|
308
|
-
for (const item of entry.lastByModelType.values()) {
|
|
309
|
-
addTypeValue(totals, item.type, item.value);
|
|
310
|
-
}
|
|
311
|
-
summaries.push({
|
|
312
|
-
sessionId,
|
|
313
|
-
sessionStartMs: entry.sessionStartMs,
|
|
314
|
-
models: entry.models,
|
|
315
|
-
input: totals.input,
|
|
316
|
-
output: totals.output,
|
|
317
|
-
thought: totals.thought,
|
|
318
|
-
cache: totals.cache,
|
|
319
|
-
tool: totals.tool
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
summaries.sort((a, b) => a.sessionStartMs - b.sessionStartMs || a.sessionId.localeCompare(b.sessionId));
|
|
323
|
-
return summaries;
|
|
324
|
-
}
|
|
325
|
-
function toSessionTotals(summary) {
|
|
326
|
-
return {
|
|
327
|
-
date: toDateKey(summary.sessionStartMs),
|
|
328
|
-
sessionId: summary.sessionId,
|
|
329
|
-
models: summary.models,
|
|
330
|
-
input: summary.input,
|
|
331
|
-
output: summary.output,
|
|
332
|
-
thought: summary.thought,
|
|
333
|
-
cache: summary.cache,
|
|
334
|
-
tool: summary.tool
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
function aggregateSessionsToDay(summaries) {
|
|
338
|
-
const map = new Map();
|
|
339
|
-
for (const s of summaries) {
|
|
340
|
-
const dateKey = toDateKey(s.sessionStartMs);
|
|
341
|
-
let row = map.get(dateKey);
|
|
342
|
-
if (!row) {
|
|
343
|
-
row = {
|
|
344
|
-
date: dateKey,
|
|
345
|
-
models: new Set(),
|
|
346
|
-
input: 0,
|
|
347
|
-
output: 0,
|
|
348
|
-
thought: 0,
|
|
349
|
-
cache: 0,
|
|
350
|
-
tool: 0
|
|
351
|
-
};
|
|
352
|
-
map.set(dateKey, row);
|
|
353
|
-
}
|
|
354
|
-
for (const m of s.models)
|
|
355
|
-
row.models.add(m);
|
|
356
|
-
row.input += s.input;
|
|
357
|
-
row.output += s.output;
|
|
358
|
-
row.thought += s.thought;
|
|
359
|
-
row.cache += s.cache;
|
|
360
|
-
row.tool += s.tool;
|
|
361
|
-
}
|
|
362
|
-
const rows = Array.from(map.values());
|
|
363
|
-
rows.sort((a, b) => a.date.localeCompare(b.date));
|
|
364
|
-
return rows;
|
|
365
|
-
}
|
|
366
|
-
function extractTokenMetricBlocks(root) {
|
|
367
|
-
const blocks = [];
|
|
368
|
-
const stack = [root];
|
|
369
|
-
while (stack.length > 0) {
|
|
370
|
-
const node = stack.pop();
|
|
371
|
-
if (!node)
|
|
372
|
-
continue;
|
|
373
|
-
if (Array.isArray(node)) {
|
|
374
|
-
for (const item of node)
|
|
375
|
-
stack.push(item);
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
if (typeof node !== "object")
|
|
379
|
-
continue;
|
|
380
|
-
const obj = node;
|
|
381
|
-
const name = typeof obj.name === "string" ? obj.name : null;
|
|
382
|
-
const descriptorName = obj.descriptor && typeof obj.descriptor === "object"
|
|
383
|
-
? typeof obj.descriptor.name === "string"
|
|
384
|
-
? obj.descriptor.name
|
|
385
|
-
: null
|
|
386
|
-
: null;
|
|
387
|
-
if (name === "gemini_cli.token.usage" || descriptorName === "gemini_cli.token.usage") {
|
|
388
|
-
const dataPoints = obj.dataPoints;
|
|
389
|
-
if (Array.isArray(dataPoints)) {
|
|
390
|
-
blocks.push({
|
|
391
|
-
descriptor: { name: "gemini_cli.token.usage" },
|
|
392
|
-
dataPoints
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
for (const value of Object.values(obj)) {
|
|
397
|
-
stack.push(value);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return blocks;
|
|
401
|
-
}
|
|
402
|
-
function extractTokenUsageDataPoints(root) {
|
|
403
|
-
const points = [];
|
|
404
|
-
const stack = [root];
|
|
405
|
-
while (stack.length > 0) {
|
|
406
|
-
const node = stack.pop();
|
|
407
|
-
if (!node)
|
|
408
|
-
continue;
|
|
409
|
-
if (Array.isArray(node)) {
|
|
410
|
-
for (const item of node)
|
|
411
|
-
stack.push(item);
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
if (typeof node !== "object")
|
|
415
|
-
continue;
|
|
416
|
-
const obj = node;
|
|
417
|
-
const name = typeof obj.name === "string" ? obj.name : null;
|
|
418
|
-
const descriptorName = obj.descriptor && typeof obj.descriptor === "object"
|
|
419
|
-
? typeof obj.descriptor.name === "string"
|
|
420
|
-
? obj.descriptor.name
|
|
421
|
-
: null
|
|
422
|
-
: null;
|
|
423
|
-
if (name === "gemini_cli.token.usage" || descriptorName === "gemini_cli.token.usage") {
|
|
424
|
-
const dataPoints = obj.dataPoints;
|
|
425
|
-
if (Array.isArray(dataPoints)) {
|
|
426
|
-
for (const dp of dataPoints) {
|
|
427
|
-
if (dp && typeof dp === "object")
|
|
428
|
-
points.push(dp);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
for (const value of Object.values(obj)) {
|
|
433
|
-
stack.push(value);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return points;
|
|
437
|
-
}
|
|
438
|
-
function readAttributeValueFromDataPoint(dp, key) {
|
|
439
|
-
const attrs = readAttributes(dp.attributes);
|
|
440
|
-
return attrs[key] || null;
|
|
441
|
-
}
|
|
442
|
-
function readSessionId(dp) {
|
|
443
|
-
const attrs = readAttributes(dp.attributes);
|
|
444
|
-
return attrs["session.id"] || attrs["session_id"] || null;
|
|
445
|
-
}
|
|
446
|
-
function buildPointFromDataPoint(dataPoint) {
|
|
447
|
-
if (!dataPoint || typeof dataPoint !== "object")
|
|
448
|
-
return null;
|
|
449
|
-
const dp = dataPoint;
|
|
450
|
-
const value = readNumberValue(dp);
|
|
451
|
-
if (value === null)
|
|
452
|
-
return null;
|
|
453
|
-
const timestampMs = readTimestampMs(dp);
|
|
454
|
-
if (timestampMs === null)
|
|
455
|
-
return null;
|
|
456
|
-
const attrs = readAttributes(dp.attributes);
|
|
457
|
-
const model = attrs.model || attrs["model"] || "unknown";
|
|
458
|
-
const type = attrs.type || attrs["type"] || "unknown";
|
|
459
|
-
const sessionId = attrs["session.id"] || attrs["session_id"] || null;
|
|
460
|
-
return {
|
|
461
|
-
timestampMs,
|
|
462
|
-
model,
|
|
463
|
-
type,
|
|
464
|
-
sessionId,
|
|
465
|
-
value
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
function readNumberValue(dp) {
|
|
469
|
-
const asInt = dp.asInt;
|
|
470
|
-
const asDouble = dp.asDouble;
|
|
471
|
-
const value = dp.value;
|
|
472
|
-
if (typeof asInt === "number")
|
|
473
|
-
return asInt;
|
|
474
|
-
if (typeof asDouble === "number")
|
|
475
|
-
return asDouble;
|
|
476
|
-
if (typeof value === "number")
|
|
477
|
-
return value;
|
|
478
|
-
if (typeof asInt === "string") {
|
|
479
|
-
const n = Number(asInt);
|
|
480
|
-
if (!Number.isNaN(n))
|
|
481
|
-
return n;
|
|
482
|
-
}
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
function readTimestampMs(dp) {
|
|
486
|
-
const timeUnixNano = dp.timeUnixNano;
|
|
487
|
-
const endTimeUnixNano = dp.endTimeUnixNano;
|
|
488
|
-
const timeUnix = dp.timeUnix;
|
|
489
|
-
const time = dp.time;
|
|
490
|
-
const endTime = dp.endTime;
|
|
491
|
-
const startTime = dp.startTime;
|
|
492
|
-
if (Array.isArray(endTime) && endTime.length >= 2) {
|
|
493
|
-
const sec = Number(endTime[0]);
|
|
494
|
-
const nsec = Number(endTime[1]);
|
|
495
|
-
if (!Number.isNaN(sec) && !Number.isNaN(nsec)) {
|
|
496
|
-
return sec * 1000 + Math.floor(nsec / 1e6);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (Array.isArray(startTime) && startTime.length >= 2) {
|
|
500
|
-
const sec = Number(startTime[0]);
|
|
501
|
-
const nsec = Number(startTime[1]);
|
|
502
|
-
if (!Number.isNaN(sec) && !Number.isNaN(nsec)) {
|
|
503
|
-
return sec * 1000 + Math.floor(nsec / 1e6);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
const candidate = typeof timeUnixNano === "string" || typeof timeUnixNano === "number"
|
|
507
|
-
? timeUnixNano
|
|
508
|
-
: typeof endTimeUnixNano === "string" || typeof endTimeUnixNano === "number"
|
|
509
|
-
? endTimeUnixNano
|
|
510
|
-
: typeof timeUnix === "string" || typeof timeUnix === "number"
|
|
511
|
-
? timeUnix
|
|
512
|
-
: typeof time === "string" || typeof time === "number"
|
|
513
|
-
? time
|
|
514
|
-
: null;
|
|
515
|
-
if (candidate === null)
|
|
516
|
-
return null;
|
|
517
|
-
const num = Number(candidate);
|
|
518
|
-
if (Number.isNaN(num))
|
|
519
|
-
return null;
|
|
520
|
-
if (num > 1e15) {
|
|
521
|
-
return Math.floor(num / 1e6);
|
|
522
|
-
}
|
|
523
|
-
if (num > 1e12) {
|
|
524
|
-
return Math.floor(num / 1e3);
|
|
525
|
-
}
|
|
526
|
-
return Math.floor(num);
|
|
527
|
-
}
|
|
528
|
-
function readAttributes(attrs) {
|
|
529
|
-
if (!attrs)
|
|
530
|
-
return {};
|
|
531
|
-
if (Array.isArray(attrs)) {
|
|
532
|
-
const result = {};
|
|
533
|
-
for (const item of attrs) {
|
|
534
|
-
if (!item || typeof item !== "object")
|
|
535
|
-
continue;
|
|
536
|
-
const obj = item;
|
|
537
|
-
const key = typeof obj.key === "string" ? obj.key : null;
|
|
538
|
-
if (!key)
|
|
539
|
-
continue;
|
|
540
|
-
const value = obj.value;
|
|
541
|
-
const strValue = readAttributeValue(value);
|
|
542
|
-
if (strValue !== null)
|
|
543
|
-
result[key] = strValue;
|
|
544
|
-
}
|
|
545
|
-
return result;
|
|
546
|
-
}
|
|
547
|
-
if (typeof attrs === "object") {
|
|
548
|
-
const result = {};
|
|
549
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
550
|
-
const strValue = readAttributeValue(value);
|
|
551
|
-
if (strValue !== null)
|
|
552
|
-
result[key] = strValue;
|
|
553
|
-
}
|
|
554
|
-
return result;
|
|
555
|
-
}
|
|
556
|
-
return {};
|
|
557
|
-
}
|
|
558
|
-
function readAttributeValue(value) {
|
|
559
|
-
if (typeof value === "string")
|
|
560
|
-
return value;
|
|
561
|
-
if (typeof value === "number")
|
|
562
|
-
return String(value);
|
|
563
|
-
if (!value || typeof value !== "object")
|
|
564
|
-
return null;
|
|
565
|
-
const obj = value;
|
|
566
|
-
const stringValue = obj.stringValue;
|
|
567
|
-
if (typeof stringValue === "string")
|
|
568
|
-
return stringValue;
|
|
569
|
-
const intValue = obj.intValue;
|
|
570
|
-
if (typeof intValue === "string" || typeof intValue === "number") {
|
|
571
|
-
return String(intValue);
|
|
572
|
-
}
|
|
573
|
-
const doubleValue = obj.doubleValue;
|
|
574
|
-
if (typeof doubleValue === "string" || typeof doubleValue === "number") {
|
|
575
|
-
return String(doubleValue);
|
|
576
|
-
}
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
function aggregateByDay(points) {
|
|
580
|
-
const map = new Map();
|
|
581
|
-
for (const p of points) {
|
|
582
|
-
const dateKey = toDateKey(p.timestampMs);
|
|
583
|
-
let row = map.get(dateKey);
|
|
584
|
-
if (!row) {
|
|
585
|
-
row = {
|
|
586
|
-
date: dateKey,
|
|
587
|
-
models: new Set(),
|
|
588
|
-
input: 0,
|
|
589
|
-
output: 0,
|
|
590
|
-
thought: 0,
|
|
591
|
-
cache: 0,
|
|
592
|
-
tool: 0
|
|
593
|
-
};
|
|
594
|
-
map.set(dateKey, row);
|
|
595
|
-
}
|
|
596
|
-
row.models.add(p.model);
|
|
597
|
-
addTypeValue(row, p.type, p.value);
|
|
598
|
-
}
|
|
599
|
-
const rows = Array.from(map.values());
|
|
600
|
-
rows.sort((a, b) => a.date.localeCompare(b.date));
|
|
601
|
-
return rows;
|
|
602
|
-
}
|
|
603
|
-
function aggregateBySession(points) {
|
|
604
|
-
const map = new Map();
|
|
605
|
-
for (const p of points) {
|
|
606
|
-
if (!p.sessionId)
|
|
607
|
-
continue;
|
|
608
|
-
const dateKey = toDateKey(p.timestampMs);
|
|
609
|
-
const key = `${dateKey}||${p.sessionId}`;
|
|
610
|
-
let row = map.get(key);
|
|
611
|
-
if (!row) {
|
|
612
|
-
row = {
|
|
613
|
-
date: dateKey,
|
|
614
|
-
sessionId: p.sessionId,
|
|
615
|
-
models: new Set(),
|
|
616
|
-
input: 0,
|
|
617
|
-
output: 0,
|
|
618
|
-
thought: 0,
|
|
619
|
-
cache: 0,
|
|
620
|
-
tool: 0
|
|
621
|
-
};
|
|
622
|
-
map.set(key, row);
|
|
623
|
-
}
|
|
624
|
-
row.models.add(p.model);
|
|
625
|
-
addTypeValue(row, p.type, p.value);
|
|
626
|
-
}
|
|
627
|
-
const rows = Array.from(map.values());
|
|
628
|
-
rows.sort((a, b) => {
|
|
629
|
-
if (a.date !== b.date)
|
|
630
|
-
return a.date.localeCompare(b.date);
|
|
631
|
-
return a.sessionId.localeCompare(b.sessionId);
|
|
632
|
-
});
|
|
633
|
-
return rows;
|
|
634
|
-
}
|
|
635
|
-
function addTypeValue(target, type, value) {
|
|
636
|
-
if (type === "input")
|
|
637
|
-
target.input += value;
|
|
638
|
-
else if (type === "output")
|
|
639
|
-
target.output += value;
|
|
640
|
-
else if (type === "thought")
|
|
641
|
-
target.thought += value;
|
|
642
|
-
else if (type === "cache")
|
|
643
|
-
target.cache += value;
|
|
644
|
-
else if (type === "tool")
|
|
645
|
-
target.tool += value;
|
|
646
|
-
}
|
|
647
|
-
function toDateKey(timestampMs) {
|
|
648
|
-
const date = new Date(timestampMs);
|
|
649
|
-
const year = date.getFullYear();
|
|
650
|
-
const month = pad2(date.getMonth() + 1);
|
|
651
|
-
const day = pad2(date.getDate());
|
|
652
|
-
return `${year}-${month}-${day}`;
|
|
653
|
-
}
|
|
654
|
-
function pad2(n) {
|
|
655
|
-
return n < 10 ? `0${n}` : String(n);
|
|
656
|
-
}
|
|
657
|
-
function normalizePeriod(value) {
|
|
658
|
-
if (value === "day" || value === "week" || value === "month" || value === "session")
|
|
659
|
-
return value;
|
|
660
|
-
return null;
|
|
661
|
-
}
|
|
662
|
-
function parseTimeSpec(input, kind) {
|
|
663
|
-
if (typeof input !== "string")
|
|
664
|
-
return null;
|
|
665
|
-
const raw = input.trim().toLowerCase();
|
|
666
|
-
if (!raw)
|
|
667
|
-
return null;
|
|
668
|
-
const now = new Date();
|
|
669
|
-
if (raw === "today") {
|
|
670
|
-
return kind === "since" ? startOfDay(now).getTime() : endOfDay(now).getTime();
|
|
671
|
-
}
|
|
672
|
-
if (raw === "yesterday") {
|
|
673
|
-
const d = new Date(now.getTime() - 86400000);
|
|
674
|
-
return kind === "since" ? startOfDay(d).getTime() : endOfDay(d).getTime();
|
|
675
|
-
}
|
|
676
|
-
const relMatch = raw.match(/^(\d+)([dh])$/);
|
|
677
|
-
if (relMatch) {
|
|
678
|
-
const amount = Number(relMatch[1]);
|
|
679
|
-
const unit = relMatch[2];
|
|
680
|
-
if (!Number.isNaN(amount)) {
|
|
681
|
-
const delta = unit === "d" ? amount * 86400000 : amount * 3600000;
|
|
682
|
-
return now.getTime() - delta;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
const parsed = new Date(raw);
|
|
686
|
-
if (!Number.isNaN(parsed.getTime())) {
|
|
687
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
688
|
-
return kind === "since" ? startOfDay(parsed).getTime() : endOfDay(parsed).getTime();
|
|
689
|
-
}
|
|
690
|
-
return parsed.getTime();
|
|
691
|
-
}
|
|
692
|
-
console.error(`无法解析时间参数:${input}`);
|
|
693
|
-
process.exit(1);
|
|
694
|
-
}
|
|
695
|
-
function startOfDay(d) {
|
|
696
|
-
const tmp = new Date(d.getTime());
|
|
697
|
-
tmp.setHours(0, 0, 0, 0);
|
|
698
|
-
return tmp;
|
|
699
|
-
}
|
|
700
|
-
function endOfDay(d) {
|
|
701
|
-
const tmp = new Date(d.getTime());
|
|
702
|
-
tmp.setHours(23, 59, 59, 999);
|
|
703
|
-
return tmp;
|
|
704
|
-
}
|
|
705
|
-
function renderTable(result, period) {
|
|
706
|
-
if (result.table.length === 0) {
|
|
707
|
-
console.log("无匹配数据");
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
if (period === "session") {
|
|
711
|
-
const rows = result.table;
|
|
712
|
-
renderSessionTable(rows, result.range);
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
const rows = result.table;
|
|
716
|
-
renderDailyTable(rows, result.range);
|
|
717
|
-
}
|
|
718
|
-
function renderDailyTable(rows, range) {
|
|
719
|
-
const headers = ["Date", "Models", "Input", "Output", "Thought", "Cache", "Tool", "Total Tokens"];
|
|
720
|
-
const widths = headers.map((h) => h.length);
|
|
721
|
-
const totals = { input: 0, output: 0, thought: 0, cache: 0, tool: 0 };
|
|
722
|
-
for (const row of rows) {
|
|
723
|
-
const modelsText = Array.from(row.models);
|
|
724
|
-
widths[0] = Math.max(widths[0], row.date.length);
|
|
725
|
-
widths[1] = Math.max(widths[1], maxLineWidth(modelsText));
|
|
726
|
-
widths[2] = Math.max(widths[2], formatNumber(row.input).length);
|
|
727
|
-
widths[3] = Math.max(widths[3], formatNumber(row.output).length);
|
|
728
|
-
widths[4] = Math.max(widths[4], formatNumber(row.thought).length);
|
|
729
|
-
widths[5] = Math.max(widths[5], formatNumber(row.cache).length);
|
|
730
|
-
widths[6] = Math.max(widths[6], formatNumber(row.tool).length);
|
|
731
|
-
widths[7] = Math.max(widths[7], formatNumber(sumAll(row)).length);
|
|
732
|
-
totals.input += row.input;
|
|
733
|
-
totals.output += row.output;
|
|
734
|
-
totals.thought += row.thought;
|
|
735
|
-
totals.cache += row.cache;
|
|
736
|
-
totals.tool += row.tool;
|
|
737
|
-
}
|
|
738
|
-
const lines = [];
|
|
739
|
-
lines.push(...renderTitleBlock(rows, range));
|
|
740
|
-
lines.push(colorize(headers, "header", widths));
|
|
741
|
-
lines.push("");
|
|
742
|
-
for (const row of rows) {
|
|
743
|
-
const modelLines = Array.from(row.models);
|
|
744
|
-
lines.push(...formatRowMulti([
|
|
745
|
-
[row.date],
|
|
746
|
-
modelLines.length > 0 ? modelLines : [""],
|
|
747
|
-
[formatNumber(row.input)],
|
|
748
|
-
[formatNumber(row.output)],
|
|
749
|
-
[formatNumber(row.thought)],
|
|
750
|
-
[formatNumber(row.cache)],
|
|
751
|
-
[formatNumber(row.tool)],
|
|
752
|
-
[formatNumber(sumAll(row))]
|
|
753
|
-
], widths, [false, false, true, true, true, true, true, true]));
|
|
754
|
-
lines.push("");
|
|
755
|
-
}
|
|
756
|
-
const totalCells = [
|
|
757
|
-
["Total"],
|
|
758
|
-
[""],
|
|
759
|
-
[formatCompact(totals.input)],
|
|
760
|
-
[formatCompact(totals.output)],
|
|
761
|
-
[formatCompact(totals.thought)],
|
|
762
|
-
[formatCompact(totals.cache)],
|
|
763
|
-
[formatCompact(totals.tool)],
|
|
764
|
-
[formatCompact(sumTotals(totals))]
|
|
765
|
-
];
|
|
766
|
-
const totalLines = formatRowMulti(totalCells, widths, [false, false, true, true, true, true, true, true]);
|
|
767
|
-
lines.push(colorizeLines(totalLines, "total"));
|
|
768
|
-
lines.push("");
|
|
769
|
-
console.log(lines.join("\n"));
|
|
770
|
-
}
|
|
771
|
-
function renderSessionTable(rows, range) {
|
|
772
|
-
const headers = ["Date", "Session", "Models", "Input", "Output", "Thought", "Cache", "Tool", "Total Tokens"];
|
|
773
|
-
const widths = headers.map((h) => h.length);
|
|
774
|
-
const totals = { input: 0, output: 0, thought: 0, cache: 0, tool: 0 };
|
|
775
|
-
for (const row of rows) {
|
|
776
|
-
const modelsText = Array.from(row.models);
|
|
777
|
-
widths[0] = Math.max(widths[0], row.date.length);
|
|
778
|
-
widths[1] = Math.max(widths[1], row.sessionId.length);
|
|
779
|
-
widths[2] = Math.max(widths[2], maxLineWidth(modelsText));
|
|
780
|
-
widths[3] = Math.max(widths[3], formatNumber(row.input).length);
|
|
781
|
-
widths[4] = Math.max(widths[4], formatNumber(row.output).length);
|
|
782
|
-
widths[5] = Math.max(widths[5], formatNumber(row.thought).length);
|
|
783
|
-
widths[6] = Math.max(widths[6], formatNumber(row.cache).length);
|
|
784
|
-
widths[7] = Math.max(widths[7], formatNumber(row.tool).length);
|
|
785
|
-
widths[8] = Math.max(widths[8], formatNumber(sumAll(row)).length);
|
|
786
|
-
totals.input += row.input;
|
|
787
|
-
totals.output += row.output;
|
|
788
|
-
totals.thought += row.thought;
|
|
789
|
-
totals.cache += row.cache;
|
|
790
|
-
totals.tool += row.tool;
|
|
791
|
-
}
|
|
792
|
-
const lines = [];
|
|
793
|
-
lines.push(...renderTitleBlock(rows, range));
|
|
794
|
-
lines.push(colorize(headers, "header", widths));
|
|
795
|
-
lines.push("");
|
|
796
|
-
for (const row of rows) {
|
|
797
|
-
const modelLines = Array.from(row.models);
|
|
798
|
-
lines.push(...formatRowMulti([
|
|
799
|
-
[row.date],
|
|
800
|
-
[row.sessionId],
|
|
801
|
-
modelLines.length > 0 ? modelLines : [""],
|
|
802
|
-
[formatNumber(row.input)],
|
|
803
|
-
[formatNumber(row.output)],
|
|
804
|
-
[formatNumber(row.thought)],
|
|
805
|
-
[formatNumber(row.cache)],
|
|
806
|
-
[formatNumber(row.tool)],
|
|
807
|
-
[formatNumber(sumAll(row))]
|
|
808
|
-
], widths, [false, false, false, true, true, true, true, true, true]));
|
|
809
|
-
lines.push("");
|
|
810
|
-
}
|
|
811
|
-
const totalCells = [
|
|
812
|
-
["Total"],
|
|
813
|
-
[""],
|
|
814
|
-
[""],
|
|
815
|
-
[formatCompact(totals.input)],
|
|
816
|
-
[formatCompact(totals.output)],
|
|
817
|
-
[formatCompact(totals.thought)],
|
|
818
|
-
[formatCompact(totals.cache)],
|
|
819
|
-
[formatCompact(totals.tool)],
|
|
820
|
-
[formatCompact(sumTotals(totals))]
|
|
821
|
-
];
|
|
822
|
-
const totalLines = formatRowMulti(totalCells, widths, [false, false, false, true, true, true, true, true, true]);
|
|
823
|
-
lines.push(colorizeLines(totalLines, "total"));
|
|
824
|
-
lines.push("");
|
|
825
|
-
console.log(lines.join("\n"));
|
|
826
|
-
}
|
|
827
|
-
function formatRow(cells, widths, align) {
|
|
828
|
-
return cells
|
|
829
|
-
.map((cell, i) => {
|
|
830
|
-
const width = widths[i];
|
|
831
|
-
if (align && align[i]) {
|
|
832
|
-
return cell.padStart(width);
|
|
833
|
-
}
|
|
834
|
-
return cell.padEnd(width);
|
|
835
|
-
})
|
|
836
|
-
.join(" ");
|
|
837
|
-
}
|
|
838
|
-
function formatRowMulti(cells, widths, align) {
|
|
839
|
-
const height = cells.reduce((max, col) => Math.max(max, col.length), 1);
|
|
840
|
-
const lines = [];
|
|
841
|
-
for (let rowIndex = 0; rowIndex < height; rowIndex += 1) {
|
|
842
|
-
const rowCells = cells.map((col) => (rowIndex < col.length ? col[rowIndex] : ""));
|
|
843
|
-
lines.push(formatRow(rowCells, widths, align));
|
|
844
|
-
}
|
|
845
|
-
return lines;
|
|
846
|
-
}
|
|
847
|
-
function maxLineWidth(lines) {
|
|
848
|
-
if (lines.length === 0)
|
|
849
|
-
return 0;
|
|
850
|
-
return lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
851
|
-
}
|
|
852
|
-
function formatNumber(value) {
|
|
853
|
-
return Math.round(value).toLocaleString("en-US");
|
|
854
|
-
}
|
|
855
|
-
function formatCompact(value) {
|
|
856
|
-
const abs = Math.abs(value);
|
|
857
|
-
if (abs >= 1e9)
|
|
858
|
-
return `${(value / 1e9).toFixed(2)}B`;
|
|
859
|
-
if (abs >= 1e6)
|
|
860
|
-
return `${(value / 1e6).toFixed(2)}M`;
|
|
861
|
-
if (abs >= 1e3)
|
|
862
|
-
return `${(value / 1e3).toFixed(2)}k`;
|
|
863
|
-
return formatNumber(value);
|
|
864
|
-
}
|
|
865
|
-
function sumAll(row) {
|
|
866
|
-
return row.input + row.output + row.thought + row.cache + row.tool;
|
|
867
|
-
}
|
|
868
|
-
function sumTotals(totals) {
|
|
869
|
-
return totals.input + totals.output + totals.thought + totals.cache + totals.tool;
|
|
870
|
-
}
|
|
871
|
-
function colorize(headers, kind, widths) {
|
|
872
|
-
const raw = formatRow(headers, widths);
|
|
873
|
-
return applyColor(raw, kind);
|
|
874
|
-
}
|
|
875
|
-
function colorizeLines(lines, kind) {
|
|
876
|
-
return applyColor(lines.join("\n"), kind);
|
|
877
|
-
}
|
|
878
|
-
function applyColor(text, kind) {
|
|
879
|
-
const code = kind === "header" ? "\u001b[36m" : "\u001b[33m";
|
|
880
|
-
const reset = "\u001b[0m";
|
|
881
|
-
return `${code}${text}${reset}`;
|
|
882
|
-
}
|
|
883
|
-
function renderTitleBlock(rows, range) {
|
|
884
|
-
const rangeText = buildRangeText(rows, range);
|
|
885
|
-
const title = rangeText ? `Gemini-cli Usage Report - ${rangeText}` : "Gemini-cli Usage Report";
|
|
886
|
-
const border = `+${"-".repeat(title.length + 2)}+`;
|
|
887
|
-
const line = `| ${title} |`;
|
|
888
|
-
return ["", applyColor(border, "header"), applyColor(line, "header"), applyColor(border, "header"), ""];
|
|
889
|
-
}
|
|
890
|
-
function buildRangeText(rows, range) {
|
|
891
|
-
const [start, end] = resolveDateRange(rows, range);
|
|
892
|
-
if (!start || !end)
|
|
893
|
-
return "";
|
|
894
|
-
return start === end ? start : `${start} ~ ${end}`;
|
|
895
|
-
}
|
|
896
|
-
function resolveDateRange(rows, range) {
|
|
897
|
-
if (range.sinceMs !== null && range.untilMs !== null) {
|
|
898
|
-
const start = toDateKey(range.sinceMs);
|
|
899
|
-
const end = toDateKey(range.untilMs);
|
|
900
|
-
return [start, end];
|
|
901
|
-
}
|
|
902
|
-
const dates = rows.map((r) => r.date).sort();
|
|
903
|
-
if (dates.length === 0)
|
|
904
|
-
return [null, null];
|
|
905
|
-
return [dates[0], dates[dates.length - 1]];
|
|
906
|
-
}
|
|
907
|
-
function isPeriodProvided(args) {
|
|
908
|
-
return args.some((arg) => arg === "--period" || arg.startsWith("--period="));
|
|
909
|
-
}
|
|
910
|
-
function resolveRange(period, periodWasProvided, sinceMs, untilMs) {
|
|
911
|
-
const now = new Date();
|
|
912
|
-
if (period === "session") {
|
|
913
|
-
const start = startOfDay(now).getTime();
|
|
914
|
-
const end = endOfDay(now).getTime();
|
|
915
|
-
return applyUntilClamp({ sinceMs: start, untilMs: end }, untilMs);
|
|
916
|
-
}
|
|
917
|
-
if (!periodWasProvided && sinceMs === null && untilMs === null) {
|
|
918
|
-
const end = endOfDay(now).getTime();
|
|
919
|
-
const startDate = new Date(now.getTime());
|
|
920
|
-
startDate.setDate(startDate.getDate() - 5);
|
|
921
|
-
const start = startOfDay(startDate).getTime();
|
|
922
|
-
return { sinceMs: start, untilMs: end };
|
|
923
|
-
}
|
|
924
|
-
if (period === "day") {
|
|
925
|
-
if (sinceMs === null && untilMs === null) {
|
|
926
|
-
const start = startOfDay(now).getTime();
|
|
927
|
-
const end = endOfDay(now).getTime();
|
|
928
|
-
return { sinceMs: start, untilMs: end };
|
|
929
|
-
}
|
|
930
|
-
return { sinceMs, untilMs };
|
|
931
|
-
}
|
|
932
|
-
if (period === "week") {
|
|
933
|
-
if (sinceMs !== null) {
|
|
934
|
-
const start = startOfDay(new Date(sinceMs)).getTime();
|
|
935
|
-
const end = endOfDay(new Date(start + 6 * 86400000)).getTime();
|
|
936
|
-
return applyUntilClamp({ sinceMs: start, untilMs: end }, untilMs);
|
|
937
|
-
}
|
|
938
|
-
const start = startOfWeekMonday(now).getTime();
|
|
939
|
-
const end = endOfDay(new Date(start + 6 * 86400000)).getTime();
|
|
940
|
-
return applyUntilClamp({ sinceMs: start, untilMs: end }, untilMs);
|
|
941
|
-
}
|
|
942
|
-
if (period === "month") {
|
|
943
|
-
const base = sinceMs !== null ? new Date(sinceMs) : now;
|
|
944
|
-
const start = new Date(base.getFullYear(), base.getMonth(), 1);
|
|
945
|
-
const end = new Date(base.getFullYear(), base.getMonth() + 1, 0);
|
|
946
|
-
return applyUntilClamp({ sinceMs: startOfDay(start).getTime(), untilMs: endOfDay(end).getTime() }, untilMs);
|
|
947
|
-
}
|
|
948
|
-
return { sinceMs, untilMs };
|
|
949
|
-
}
|
|
950
|
-
function applyUntilClamp(range, untilMs) {
|
|
951
|
-
if (untilMs === null)
|
|
952
|
-
return range;
|
|
953
|
-
if (range.untilMs === null || untilMs < range.untilMs) {
|
|
954
|
-
return { sinceMs: range.sinceMs, untilMs };
|
|
955
|
-
}
|
|
956
|
-
return range;
|
|
957
|
-
}
|
|
958
|
-
function startOfWeekMonday(date) {
|
|
959
|
-
const d = new Date(date.getTime());
|
|
960
|
-
const day = (d.getDay() + 6) % 7;
|
|
961
|
-
d.setDate(d.getDate() - day);
|
|
962
|
-
d.setHours(0, 0, 0, 0);
|
|
963
|
-
return d;
|
|
964
|
-
}
|