gcusage 0.1.0

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 (3) hide show
  1. package/README.md +122 -0
  2. package/dist/index.js +964 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # gcusage
2
+
3
+ Gemini CLI 用量统计工具 / Usage report for Gemini CLI
4
+
5
+ ## 简介 / Overview
6
+
7
+ - 读取 `~/.gemini/telemetry.log`,统计 token 使用量
8
+ - 仅统计每个 session 的最终累计值
9
+ - 支持 day/week/month/session 视图
10
+ - 支持日志瘦身(只保留 token 相关数据)
11
+
12
+ ## 前置配置 / Prerequisites
13
+
14
+ 在 `~/.gemini/settings.json` 启用 telemetry 并写入**绝对路径**:
15
+
16
+ ```json
17
+ {
18
+ "telemetry": {
19
+ "enabled": true,
20
+ "target": "local",
21
+ "otlpEndpoint": "",
22
+ "outfile": "/Users/enxianzhou/.gemini/telemetry.log",
23
+ "logPrompts": false
24
+ }
25
+ }
26
+ ```
27
+
28
+ ## 安装与构建 / Install & Build
29
+
30
+ ```bash
31
+ npm install
32
+ npm run build
33
+ ```
34
+
35
+ ## 基本用法 / Basic Usage
36
+
37
+ 默认输出最近 6 天(含今天)的日统计:
38
+
39
+ ```bash
40
+ node dist/index.js
41
+ ```
42
+
43
+ 输出:每天一行,展示 Models 列(多模型换行)与各类型 token 总量。
44
+
45
+ 按 session 输出(当天每个 session 一行):
46
+
47
+ ```bash
48
+ node dist/index.js --period session
49
+ ```
50
+
51
+ 输出:当天每个 session 的最终累计值。
52
+
53
+ 按周(显示该周内每天数据):
54
+
55
+ ```bash
56
+ node dist/index.js --period week
57
+ ```
58
+
59
+ 输出:当前周(周一开始)内每日数据。
60
+
61
+ 按月(显示该月内每天数据):
62
+
63
+ ```bash
64
+ node dist/index.js --period month
65
+ ```
66
+
67
+ 输出:当前月内每日数据。
68
+
69
+ 从指定日期开始统计一周:
70
+
71
+ ```bash
72
+ node dist/index.js --period week --since 2026-01-01
73
+ ```
74
+
75
+ 输出:从 2026-01-01 开始的 7 天数据。
76
+
77
+ 指定范围(覆盖 week/month 计算范围):
78
+
79
+ ```bash
80
+ node dist/index.js --period month --since 2026-01-01 --until 2026-01-15
81
+ ```
82
+
83
+ 输出:2026-01-01 到 2026-01-15 的每日数据。
84
+
85
+ 过滤模型或类型:
86
+
87
+ ```bash
88
+ node dist/index.js --model gemini-2.5-flash-lite --type input
89
+ ```
90
+
91
+ 输出:只统计指定模型与类型的数据。
92
+
93
+ ## 输出样式 / Output Format
94
+
95
+ 表头:
96
+
97
+ ```
98
+ Date | Models | Input | Output | Thought | Cache | Tool | Total Tokens
99
+ ```
100
+
101
+ 说明:
102
+
103
+ - Models 多模型换行显示
104
+ - 数字使用千分位
105
+ - Total 行使用 k/M/B 两位小数缩写
106
+ - 表头与标题为青蓝色,Total 行为金黄色
107
+ - 表格上方显示标题与日期范围
108
+
109
+ ## 日志瘦身 / Log Trim
110
+
111
+ 仅保留 token 相关数据(覆盖原文件):
112
+
113
+ ```bash
114
+ node dist/index.js trim
115
+ ```
116
+
117
+ 输出:`telemetry.log` 体积显著减小,统计不受影响。
118
+
119
+ 说明:
120
+
121
+ - 会先备份 `telemetry.log.bak`,成功后删除备份
122
+ - 只保留每个 session+model+type 的最后一次累计值
package/dist/index.js ADDED
@@ -0,0 +1,964 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ 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
+ const commander_1 = require("commander");
11
+ const program = new commander_1.Command();
12
+ let didRunSubcommand = false;
13
+ program
14
+ .name("gcusage")
15
+ .description("统计 Gemini CLI token 使用情况")
16
+ .option("--since <time>", "开始时间,支持相对时间")
17
+ .option("--until <time>", "结束时间,支持相对时间")
18
+ .option("--period <period>", "聚合周期:day|week|month|session", "day")
19
+ .option("--model <model>", "按模型过滤")
20
+ .option("--type <type>", "按类型过滤")
21
+ .option("--json", "输出 JSON")
22
+ .action(async (opts) => {
23
+ const periodValue = normalizePeriod(opts.period);
24
+ if (!periodValue) {
25
+ console.error("非法的 --period 参数,仅支持 day|week|month|session");
26
+ process.exit(1);
27
+ }
28
+ const period = periodValue;
29
+ const sinceMs = parseTimeSpec(opts.since, "since");
30
+ const untilMs = parseTimeSpec(opts.until, "until");
31
+ const periodProvided = isPeriodProvided(process.argv);
32
+ if (sinceMs !== null && untilMs !== null && sinceMs > untilMs) {
33
+ console.error("--since 不能晚于 --until");
34
+ process.exit(1);
35
+ }
36
+ const modelFilter = typeof opts.model === "string" ? opts.model : null;
37
+ const typeFilter = typeof opts.type === "string" ? opts.type : null;
38
+ const outputJson = Boolean(opts.json);
39
+ const result = await run(period, periodProvided, sinceMs, untilMs, modelFilter, typeFilter);
40
+ if (outputJson) {
41
+ process.stdout.write(JSON.stringify(result.json, null, 2));
42
+ process.stdout.write("\n");
43
+ return;
44
+ }
45
+ renderTable(result, period);
46
+ });
47
+ program
48
+ .command("trim")
49
+ .description("瘦身 telemetry.log,仅保留 token 使用数据")
50
+ .action(async () => {
51
+ didRunSubcommand = true;
52
+ await runTrim();
53
+ });
54
+ main().catch((err) => {
55
+ console.error(err instanceof Error ? err.message : String(err));
56
+ process.exit(1);
57
+ });
58
+ async function main() {
59
+ await program.parseAsync(process.argv);
60
+ if (didRunSubcommand)
61
+ return;
62
+ }
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
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "gcusage",
3
+ "version": "0.1.0",
4
+ "description": "Gemini CLI usage statistics",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "gcusage": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "dependencies": {
18
+ "commander": "^12.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.11.0",
22
+ "typescript": "^5.3.3"
23
+ }
24
+ }