ccus-cli 0.1.2 → 0.1.4
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 +178 -173
- package/dist/cli.js +33 -16
- package/dist/lib/aggregate-dashboard.js +443 -418
- package/dist/lib/dashboard.js +176 -42
- package/dist/lib/git.js +23 -4
- package/dist/lib/payload.js +11 -4
- package/dist/lib/time.js +23 -0
- package/package.json +35 -35
package/dist/lib/dashboard.js
CHANGED
|
@@ -46,12 +46,13 @@ function summarizeEvents(events) {
|
|
|
46
46
|
*/
|
|
47
47
|
function bucketizeEvents(events, start, end, bucketMinutes = 5) {
|
|
48
48
|
const bucketMs = bucketMinutes * 60 * 1000;
|
|
49
|
+
// 5h 与 7d 在同一时间桶里各自独立收集:某条采样可能只带其中一项,不应互相牵连。
|
|
49
50
|
const buckets = new Map();
|
|
50
51
|
for (let cursor = start.getTime(); cursor <= end.getTime(); cursor += bucketMs) {
|
|
51
|
-
buckets.set(cursor, []);
|
|
52
|
+
buckets.set(cursor, { fiveHour: [], sevenDay: [] });
|
|
52
53
|
}
|
|
53
54
|
for (const event of events) {
|
|
54
|
-
if (event.usagePct === null) {
|
|
55
|
+
if (event.usagePct === null && event.sevenDayUsagePct === null) {
|
|
55
56
|
continue;
|
|
56
57
|
}
|
|
57
58
|
const ts = new Date(event.timestamp).getTime();
|
|
@@ -59,17 +60,24 @@ function bucketizeEvents(events, start, end, bucketMinutes = 5) {
|
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
const bucketStart = start.getTime() + Math.floor((ts - start.getTime()) / bucketMs) * bucketMs;
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
63
|
+
const slot = buckets.get(bucketStart);
|
|
64
|
+
if (!slot) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (event.usagePct !== null) {
|
|
68
|
+
slot.fiveHour.push(event.usagePct);
|
|
69
|
+
}
|
|
70
|
+
if (event.sevenDayUsagePct !== null) {
|
|
71
|
+
slot.sevenDay.push(event.sevenDayUsagePct);
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
|
-
return [...buckets.entries()].map(([bucketStart,
|
|
74
|
+
return [...buckets.entries()].map(([bucketStart, slot]) => ({
|
|
68
75
|
bucketStart: new Date(bucketStart).toISOString(),
|
|
69
|
-
avgUsagePct:
|
|
70
|
-
maxUsagePct:
|
|
71
|
-
minUsagePct:
|
|
72
|
-
|
|
76
|
+
avgUsagePct: slot.fiveHour.length > 0 ? (0, time_1.roundNumber)(average(slot.fiveHour), 1) : null,
|
|
77
|
+
maxUsagePct: slot.fiveHour.length > 0 ? (0, time_1.roundNumber)(Math.max(...slot.fiveHour), 1) : null,
|
|
78
|
+
minUsagePct: slot.fiveHour.length > 0 ? (0, time_1.roundNumber)(Math.min(...slot.fiveHour), 1) : null,
|
|
79
|
+
avgSevenDayUsagePct: slot.sevenDay.length > 0 ? (0, time_1.roundNumber)(average(slot.sevenDay), 1) : null,
|
|
80
|
+
sampleCount: slot.fiveHour.length,
|
|
73
81
|
}));
|
|
74
82
|
}
|
|
75
83
|
/** 直接输出内联 SVG 曲线图,避免额外引入前端框架或图表依赖。 */
|
|
@@ -80,46 +88,154 @@ function renderChart(buckets) {
|
|
|
80
88
|
const paddingY = 28;
|
|
81
89
|
const innerWidth = width - paddingX * 2;
|
|
82
90
|
const innerHeight = height - paddingY * 2;
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
const xOf = (index) => paddingX + (index / Math.max(buckets.length - 1, 1)) * innerWidth;
|
|
92
|
+
const yOf = (usage) => paddingY + ((100 - usage) / 100) * innerHeight;
|
|
93
|
+
// 每条曲线只连有数据的桶,跨空桶直接相连,避免周视图里大量空桶把线拉回 0 变成锯齿。
|
|
94
|
+
const collectPoints = (accessor) => buckets
|
|
95
|
+
.map((bucket, index) => ({ index, value: accessor(bucket), bucket }))
|
|
96
|
+
.filter((entry) => entry.value !== null)
|
|
97
|
+
.map((entry) => ({
|
|
98
|
+
x: xOf(entry.index),
|
|
99
|
+
y: yOf(entry.value),
|
|
100
|
+
usage: entry.value,
|
|
101
|
+
label: (0, time_1.formatLocalTimestamp)(new Date(entry.bucket.bucketStart)),
|
|
102
|
+
}));
|
|
103
|
+
const fiveHourPoints = collectPoints((bucket) => bucket.avgUsagePct);
|
|
104
|
+
const sevenDayPoints = collectPoints((bucket) => bucket.avgSevenDayUsagePct);
|
|
105
|
+
const linePathOf = (points) => points.map((point, index) => `${index === 0 ? "M" : "L"}${point.x.toFixed(2)} ${point.y.toFixed(2)}`).join(" ");
|
|
106
|
+
const fiveHourLine = linePathOf(fiveHourPoints);
|
|
107
|
+
const fiveHourArea = fiveHourPoints.length > 0
|
|
108
|
+
? `${fiveHourLine} L${fiveHourPoints.at(-1).x.toFixed(2)} ${(height - paddingY).toFixed(2)} L${fiveHourPoints[0].x.toFixed(2)} ${(height - paddingY).toFixed(2)} Z`
|
|
109
|
+
: "";
|
|
110
|
+
const sevenDayLine = linePathOf(sevenDayPoints);
|
|
92
111
|
const ticks = [0, 25, 50, 75, 100].map((tick) => {
|
|
93
|
-
const y =
|
|
112
|
+
const y = yOf(tick);
|
|
94
113
|
return `<g><line x1="${paddingX}" x2="${width - paddingX}" y1="${y}" y2="${y}" class="chart-grid" /><text x="8" y="${y + 4}" class="chart-axis">${tick}%</text></g>`;
|
|
95
114
|
}).join("");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
// 跨多天的窗口(this-week / last-week)x 轴按自然日打刻度,每天一格、标签只显示月-日,
|
|
116
|
+
// 更像“周视图”;当天/短窗口仍按时间桶等距取约 6 个时:分刻度。
|
|
117
|
+
const firstTs = buckets.length > 0 ? new Date(buckets[0].bucketStart).getTime() : 0;
|
|
118
|
+
const lastTs = buckets.length > 0 ? new Date(buckets.at(-1).bucketStart).getTime() : 0;
|
|
119
|
+
const multiDay = lastTs - firstTs > 2 * 24 * 60 * 60 * 1000;
|
|
120
|
+
let markerEntries;
|
|
121
|
+
if (multiDay) {
|
|
122
|
+
const seenDays = new Set();
|
|
123
|
+
markerEntries = buckets
|
|
124
|
+
.map((bucket, index) => ({ index, day: (0, time_1.localDateKey)(new Date(bucket.bucketStart)) }))
|
|
125
|
+
.filter((entry) => {
|
|
126
|
+
if (seenDays.has(entry.day)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
seenDays.add(entry.day);
|
|
130
|
+
return true;
|
|
131
|
+
})
|
|
132
|
+
.map((entry) => ({ index: entry.index, label: entry.day.slice(5) }));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
markerEntries = buckets
|
|
136
|
+
.map((bucket, index) => ({ index, label: (0, time_1.formatLocalTimestamp)(new Date(bucket.bucketStart)) }))
|
|
137
|
+
.filter((_, index) => index === buckets.length - 1 || index % Math.max(Math.floor(buckets.length / 6), 1) === 0);
|
|
138
|
+
}
|
|
139
|
+
const markers = markerEntries
|
|
140
|
+
.map((entry) => {
|
|
141
|
+
const x = xOf(entry.index);
|
|
142
|
+
return `<g><line x1="${x}" x2="${x}" y1="${height - paddingY}" y2="${height - paddingY + 6}" class="chart-axis-line" /><text x="${x}" y="${height - 2}" text-anchor="middle" class="chart-axis">${escapeHtml(entry.label)}</text></g>`;
|
|
143
|
+
})
|
|
99
144
|
.join("");
|
|
100
|
-
const
|
|
101
|
-
.
|
|
102
|
-
.map((point) => `<circle cx="${point.x}" cy="${point.y}" r="3.5" class="chart-point"><title>${escapeHtml(point.label)} · ${point.usage.toFixed(1)}%</title></circle>`)
|
|
145
|
+
const dotsOf = (points, pointClass, seriesLabel) => points
|
|
146
|
+
.map((point) => `<circle cx="${point.x.toFixed(2)}" cy="${point.y.toFixed(2)}" r="3.5" class="${pointClass}"><title>${escapeHtml(point.label)} · ${seriesLabel} ${point.usage.toFixed(1)}%</title></circle>`)
|
|
103
147
|
.join("");
|
|
104
|
-
const noData =
|
|
105
|
-
? `<div class="empty-state">当前时间窗口里还没有可绘制的 Claude
|
|
148
|
+
const noData = fiveHourPoints.length === 0 && sevenDayPoints.length === 0
|
|
149
|
+
? `<div class="empty-state">当前时间窗口里还没有可绘制的 Claude 使用率样本。</div>`
|
|
106
150
|
: "";
|
|
107
151
|
return `
|
|
108
152
|
<section class="panel chart-panel">
|
|
109
153
|
<div class="panel-header">
|
|
110
154
|
<div>
|
|
111
155
|
<p class="eyebrow">Recent Trend</p>
|
|
112
|
-
<h2>Claude
|
|
156
|
+
<h2>Claude 使用率趋势</h2>
|
|
113
157
|
</div>
|
|
114
|
-
<p class="muted">按采样时间聚合,
|
|
158
|
+
<p class="muted">按采样时间聚合,5h 来自 rate_limits.five_hour,7d 来自 rate_limits.seven_day</p>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="chart-legend">
|
|
161
|
+
<span class="legend-item"><span class="legend-swatch legend-5h"></span>5 小时使用率</span>
|
|
162
|
+
<span class="legend-item"><span class="legend-swatch legend-7d"></span>7 天使用率</span>
|
|
115
163
|
</div>
|
|
116
164
|
${noData}
|
|
117
|
-
<svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="Claude
|
|
165
|
+
<svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="Claude 使用率趋势(5h 与 7d)">
|
|
118
166
|
${ticks}
|
|
119
|
-
|
|
120
|
-
|
|
167
|
+
${fiveHourArea ? `<path d="${fiveHourArea}" class="chart-area"></path>` : ""}
|
|
168
|
+
${sevenDayLine ? `<path d="${sevenDayLine}" class="chart-line chart-line-7d"></path>` : ""}
|
|
169
|
+
${fiveHourLine ? `<path d="${fiveHourLine}" class="chart-line"></path>` : ""}
|
|
121
170
|
${markers}
|
|
122
|
-
${
|
|
171
|
+
${dotsOf(sevenDayPoints, "chart-point chart-point-7d", "7d")}
|
|
172
|
+
${dotsOf(fiveHourPoints, "chart-point", "5h")}
|
|
173
|
+
</svg>
|
|
174
|
+
</section>
|
|
175
|
+
`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 根据时间窗口跨度自适应选择曲线分桶粒度。
|
|
179
|
+
*
|
|
180
|
+
* today/5h 这类短窗口保持 5 分钟桶;this-week/last-week 这类跨多天窗口改用小时桶,
|
|
181
|
+
* 否则一周会产生上千个点,曲线既慢又糊。
|
|
182
|
+
*/
|
|
183
|
+
function pickBucketMinutes(start, end) {
|
|
184
|
+
const spanMs = end.getTime() - start.getTime();
|
|
185
|
+
const twoDaysMs = 2 * 24 * 60 * 60 * 1000;
|
|
186
|
+
return spanMs > twoDaysMs ? 60 : 5;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 每日用户消息数柱状图。
|
|
190
|
+
*
|
|
191
|
+
* 口径与导出契约的 `userMessageCount` 一致:来自 Claude 本地 transcript 的真实用户请求数,
|
|
192
|
+
* 不是 statusline 采样数,也不是 API 请求数。
|
|
193
|
+
*/
|
|
194
|
+
function renderDailyMessages(points) {
|
|
195
|
+
if (points.length === 0) {
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
const total = points.reduce((sum, point) => sum + point.userMessageCount, 0);
|
|
199
|
+
const maxCount = Math.max(...points.map((point) => point.userMessageCount), 1);
|
|
200
|
+
const width = 920;
|
|
201
|
+
const height = 280;
|
|
202
|
+
const paddingX = 36;
|
|
203
|
+
const paddingY = 28;
|
|
204
|
+
const innerWidth = width - paddingX * 2;
|
|
205
|
+
const innerHeight = height - paddingY * 2;
|
|
206
|
+
const slot = innerWidth / points.length;
|
|
207
|
+
const barWidth = Math.max(Math.min(slot * 0.62, 48), 2);
|
|
208
|
+
const bars = points
|
|
209
|
+
.map((point, index) => {
|
|
210
|
+
const cx = paddingX + slot * (index + 0.5);
|
|
211
|
+
const barHeight = (point.userMessageCount / maxCount) * innerHeight;
|
|
212
|
+
const y = paddingY + (innerHeight - barHeight);
|
|
213
|
+
const dayLabel = point.date.slice(5);
|
|
214
|
+
return `<g>
|
|
215
|
+
<rect x="${(cx - barWidth / 2).toFixed(2)}" y="${y.toFixed(2)}" width="${barWidth.toFixed(2)}" height="${Math.max(barHeight, 0).toFixed(2)}" rx="4" class="bar"><title>${escapeHtml(dayLabel)} · ${point.userMessageCount} 条</title></rect>
|
|
216
|
+
<text x="${cx.toFixed(2)}" y="${(y - 6).toFixed(2)}" text-anchor="middle" class="bar-value">${point.userMessageCount > 0 ? point.userMessageCount : ""}</text>
|
|
217
|
+
<text x="${cx.toFixed(2)}" y="${(height - 8).toFixed(2)}" text-anchor="middle" class="chart-axis">${escapeHtml(dayLabel)}</text>
|
|
218
|
+
</g>`;
|
|
219
|
+
})
|
|
220
|
+
.join("");
|
|
221
|
+
const baselineY = paddingY + innerHeight;
|
|
222
|
+
const baseline = `<line x1="${paddingX}" x2="${width - paddingX}" y1="${baselineY}" y2="${baselineY}" class="chart-axis-line" />`;
|
|
223
|
+
const noData = total === 0
|
|
224
|
+
? `<div class="empty-state">当前时间窗口里还没有统计到 Claude 用户消息。</div>`
|
|
225
|
+
: "";
|
|
226
|
+
return `
|
|
227
|
+
<section class="panel chart-panel">
|
|
228
|
+
<div class="panel-header">
|
|
229
|
+
<div>
|
|
230
|
+
<p class="eyebrow">Daily Messages</p>
|
|
231
|
+
<h2>每日用户消息数</h2>
|
|
232
|
+
</div>
|
|
233
|
+
<p class="muted">按自然日统计的真实用户请求数(口径同导出 userMessageCount),共 ${total} 条</p>
|
|
234
|
+
</div>
|
|
235
|
+
${noData}
|
|
236
|
+
<svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="每日用户消息数">
|
|
237
|
+
${baseline}
|
|
238
|
+
${bars}
|
|
123
239
|
</svg>
|
|
124
240
|
</section>
|
|
125
241
|
`;
|
|
@@ -175,9 +291,10 @@ function renderRecentEvents(events) {
|
|
|
175
291
|
*
|
|
176
292
|
* 整个 dashboard 保持“单文件可打开”,方便导出和直接分享本地结果。
|
|
177
293
|
*/
|
|
178
|
-
function buildDashboardHtml(events, rangeLabel, start, end) {
|
|
294
|
+
function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages = []) {
|
|
179
295
|
const summary = summarizeEvents(events);
|
|
180
|
-
const buckets = bucketizeEvents(events, start, end,
|
|
296
|
+
const buckets = bucketizeEvents(events, start, end, pickBucketMinutes(start, end));
|
|
297
|
+
const totalUserMessages = dailyUserMessages.reduce((sum, point) => sum + point.userMessageCount, 0);
|
|
181
298
|
return `<!doctype html>
|
|
182
299
|
<html lang="zh-CN">
|
|
183
300
|
<head>
|
|
@@ -310,6 +427,22 @@ function buildDashboardHtml(events, rangeLabel, start, end) {
|
|
|
310
427
|
.chart-area { fill: rgba(94, 234, 212, 0.14); }
|
|
311
428
|
.chart-line { fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
312
429
|
.chart-point { fill: var(--accent); }
|
|
430
|
+
.chart-line-7d { stroke: var(--warning); stroke-dasharray: 6 4; }
|
|
431
|
+
.chart-point-7d { fill: var(--warning); }
|
|
432
|
+
.chart-legend {
|
|
433
|
+
display: flex;
|
|
434
|
+
gap: 18px;
|
|
435
|
+
padding: 10px 24px 0;
|
|
436
|
+
color: var(--muted);
|
|
437
|
+
font-size: 13px;
|
|
438
|
+
}
|
|
439
|
+
.legend-item { display: inline-flex; align-items: center; gap: 8px; }
|
|
440
|
+
.legend-swatch { width: 18px; height: 0; border-top-width: 3px; border-top-style: solid; }
|
|
441
|
+
.legend-5h { border-top-color: var(--accent); }
|
|
442
|
+
.legend-7d { border-top-color: var(--warning); border-top-style: dashed; }
|
|
443
|
+
.bar { fill: rgba(94, 234, 212, 0.55); }
|
|
444
|
+
.bar:hover { fill: var(--accent); }
|
|
445
|
+
.bar-value { fill: var(--muted); font-size: 11px; }
|
|
313
446
|
.empty-state {
|
|
314
447
|
margin: 18px 24px 0;
|
|
315
448
|
padding: 16px 18px;
|
|
@@ -375,17 +508,18 @@ function buildDashboardHtml(events, rangeLabel, start, end) {
|
|
|
375
508
|
<p class="stat-note">窗口内观测到的 5 小时使用率峰值</p>
|
|
376
509
|
</article>
|
|
377
510
|
<article class="panel stat-card">
|
|
378
|
-
<h2>
|
|
379
|
-
<p class="stat-value">${summary.
|
|
380
|
-
<p class="stat-note"
|
|
511
|
+
<h2>Latest 7d usage</h2>
|
|
512
|
+
<p class="stat-value">${escapeHtml(statValue(summary.sevenDayLatestUsagePct))}</p>
|
|
513
|
+
<p class="stat-note">最新 7 天 usage 样本,峰值 ${escapeHtml(statValue(summary.sevenDayPeakUsagePct))}</p>
|
|
381
514
|
</article>
|
|
382
515
|
<article class="panel stat-card">
|
|
383
|
-
<h2
|
|
384
|
-
<p class="stat-value">${
|
|
385
|
-
<p class="stat-note"
|
|
516
|
+
<h2>用户消息数</h2>
|
|
517
|
+
<p class="stat-value">${totalUserMessages}</p>
|
|
518
|
+
<p class="stat-note">窗口内每日真实用户请求数合计</p>
|
|
386
519
|
</article>
|
|
387
520
|
</section>
|
|
388
521
|
${renderChart(buckets)}
|
|
522
|
+
${renderDailyMessages(dailyUserMessages)}
|
|
389
523
|
${renderRecentEvents(events)}
|
|
390
524
|
</main>
|
|
391
525
|
</body>
|
package/dist/lib/git.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readGitBranch = readGitBranch;
|
|
3
4
|
exports.readGitIdentity = readGitIdentity;
|
|
4
5
|
const node_child_process_1 = require("node:child_process");
|
|
5
6
|
const node_util_1 = require("node:util");
|
|
@@ -9,8 +10,8 @@ function normalizeGitValue(value) {
|
|
|
9
10
|
const trimmed = value.replaceAll(/[\r\n\0]+/g, " ").trim();
|
|
10
11
|
return trimmed.length > 0 ? trimmed : null;
|
|
11
12
|
}
|
|
12
|
-
/**
|
|
13
|
-
async function
|
|
13
|
+
/** 跑一次 git 命令并归一化其 stdout,失败一律返回 null。 */
|
|
14
|
+
async function runGitCommand(args) {
|
|
14
15
|
try {
|
|
15
16
|
const { stdout } = await execFileAsync("git", args, {
|
|
16
17
|
windowsHide: true,
|
|
@@ -21,6 +22,24 @@ async function readGitConfigValue(args) {
|
|
|
21
22
|
return null;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 读取指定工作目录当前所在的 git 分支名。
|
|
27
|
+
*
|
|
28
|
+
* 仅用于 statusline 实时展示,不落盘、不进导出契约。
|
|
29
|
+
* detached HEAD 等无分支场景下 `rev-parse` 会返回 `HEAD`,这里归一化为 null,避免误导。
|
|
30
|
+
* 读取失败(非 git 仓库、git 不存在等)一律降级为 null。
|
|
31
|
+
*/
|
|
32
|
+
async function readGitBranch(cwd) {
|
|
33
|
+
const args = ["rev-parse", "--abbrev-ref", "HEAD"];
|
|
34
|
+
if (cwd) {
|
|
35
|
+
args.unshift("-C", cwd);
|
|
36
|
+
}
|
|
37
|
+
const branch = await runGitCommand(args);
|
|
38
|
+
if (branch === null || branch === "HEAD") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return branch;
|
|
42
|
+
}
|
|
24
43
|
/**
|
|
25
44
|
* 读取全局 Git 用户名和邮箱。
|
|
26
45
|
*
|
|
@@ -28,8 +47,8 @@ async function readGitConfigValue(args) {
|
|
|
28
47
|
*/
|
|
29
48
|
async function readGitIdentity() {
|
|
30
49
|
const [globalUserName, globalUserEmail] = await Promise.all([
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
runGitCommand(["config", "--global", "--get", "user.name"]),
|
|
51
|
+
runGitCommand(["config", "--global", "--get", "user.email"]),
|
|
33
52
|
]);
|
|
34
53
|
return {
|
|
35
54
|
userName: globalUserName,
|
package/dist/lib/payload.js
CHANGED
|
@@ -151,14 +151,21 @@ function parseStatuslinePayload(input) {
|
|
|
151
151
|
*
|
|
152
152
|
* 这里必须保持单行、紧凑,避免污染 statusline 展示区域。
|
|
153
153
|
*/
|
|
154
|
-
function formatStatusLine(event) {
|
|
154
|
+
function formatStatusLine(event, gitBranch = null) {
|
|
155
155
|
const timeLabel = (0, time_1.formatClock)(new Date(event.timestamp));
|
|
156
156
|
const usageLabel = event.usagePct === null ? "5h --" : `5h ${event.usagePct.toFixed(1)}%`;
|
|
157
157
|
const sevenDayLabel = event.sevenDayUsagePct === null ? "7d --" : `7d ${event.sevenDayUsagePct.toFixed(1)}%`;
|
|
158
158
|
const contextLabel = event.contextWindowPct === null ? "ctx --" : `ctx ${event.contextWindowPct.toFixed(1)}%`;
|
|
159
159
|
const modelLabel = event.modelName ?? "model --";
|
|
160
160
|
const workspaceLabel = event.workspaceName ?? "workspace --";
|
|
161
|
-
|
|
161
|
+
const segments = [usageLabel, sevenDayLabel, contextLabel, modelLabel, workspaceLabel];
|
|
162
|
+
// 分支名是 statusline 实时读取的展示信息,仅在拿得到时追加一段;
|
|
163
|
+
// 历史日志重算时通常没有分支,省略该段比硬塞 "branch --" 更干净。
|
|
164
|
+
if (gitBranch) {
|
|
165
|
+
segments.push(`⎇ ${gitBranch}`);
|
|
166
|
+
}
|
|
167
|
+
segments.push(timeLabel);
|
|
168
|
+
return segments.join(" | ");
|
|
162
169
|
}
|
|
163
170
|
/**
|
|
164
171
|
* 基于原始 payload 创建一条最小持久化事件。
|
|
@@ -180,7 +187,7 @@ function createPersistedStatuslineEvent(payload, now = new Date()) {
|
|
|
180
187
|
*
|
|
181
188
|
* 对旧日志会优先使用 rawPayload 重新推导;若个别旧字段缺失,再回退到历史持久化字段。
|
|
182
189
|
*/
|
|
183
|
-
function computeStatuslineEvent(record) {
|
|
190
|
+
function computeStatuslineEvent(record, options = {}) {
|
|
184
191
|
const legacy = record;
|
|
185
192
|
const payload = record.rawPayload ?? {};
|
|
186
193
|
const sessionId = readSessionId(payload) ?? legacy.sessionId ?? null;
|
|
@@ -213,7 +220,7 @@ function computeStatuslineEvent(record) {
|
|
|
213
220
|
};
|
|
214
221
|
return {
|
|
215
222
|
...baseEvent,
|
|
216
|
-
statusLine: legacy.statusLine ?? formatStatusLine(baseEvent),
|
|
223
|
+
statusLine: legacy.statusLine ?? formatStatusLine(baseEvent, options.gitBranch ?? null),
|
|
217
224
|
};
|
|
218
225
|
}
|
|
219
226
|
//# sourceMappingURL=payload.js.map
|
package/dist/lib/time.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveRange = resolveRange;
|
|
4
|
+
exports.expandToFullWeekWindow = expandToFullWeekWindow;
|
|
4
5
|
exports.localDateKey = localDateKey;
|
|
5
6
|
exports.formatRangeFileLabel = formatRangeFileLabel;
|
|
6
7
|
exports.extractGitEmailAccount = extractGitEmailAccount;
|
|
@@ -27,6 +28,15 @@ function startOfLocalWeek(date) {
|
|
|
27
28
|
start.setDate(date.getDate() + diff);
|
|
28
29
|
return startOfLocalDay(start);
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* 以周日 23:59:59.999 作为一周结束(周一为起始)。
|
|
33
|
+
*
|
|
34
|
+
* 和 `startOfLocalWeek` 对称,用来把 `this-week` 补齐成完整一周。
|
|
35
|
+
*/
|
|
36
|
+
function endOfLocalWeek(date) {
|
|
37
|
+
const start = startOfLocalWeek(date);
|
|
38
|
+
return new Date(start.getFullYear(), start.getMonth(), start.getDate() + 6, 23, 59, 59, 999);
|
|
39
|
+
}
|
|
30
40
|
/** range 短别名:保持解析后的 label 仍为规范名,避免影响文件名与 bundle 契约。 */
|
|
31
41
|
const RANGE_ALIASES = {
|
|
32
42
|
lw: "last-week",
|
|
@@ -66,6 +76,19 @@ function resolveRange(range, now = new Date()) {
|
|
|
66
76
|
end: now,
|
|
67
77
|
};
|
|
68
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* 把 `this-week` 窗口补齐成完整一周(周一 ~ 周日)。
|
|
81
|
+
*
|
|
82
|
+
* export 用它保证文件名与 dailySummaries 始终覆盖整周:即使本周尚未结束、
|
|
83
|
+
* 后面几天还没有任何数据,文件名也固定是「周一_to_周日」,每天一条 daily。
|
|
84
|
+
* `last-week` 等已经是完整周的窗口原样返回,其它相对窗口不受影响。
|
|
85
|
+
*/
|
|
86
|
+
function expandToFullWeekWindow(window) {
|
|
87
|
+
if (window.label !== "this-week") {
|
|
88
|
+
return window;
|
|
89
|
+
}
|
|
90
|
+
return { ...window, end: endOfLocalWeek(window.start) };
|
|
91
|
+
}
|
|
69
92
|
/**
|
|
70
93
|
* 把日期映射成稳定的本地日键,用于事件按天分桶存储。
|
|
71
94
|
*/
|
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ccus-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Claude Code statusline usage logger and dashboard CLI",
|
|
5
|
-
"type": "commonjs",
|
|
6
|
-
"bin": {
|
|
7
|
-
"ccus": "dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist/cli.js",
|
|
11
|
-
"dist/lib/**/*.js",
|
|
12
|
-
"dist/types.js"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc -p tsconfig.json",
|
|
16
|
-
"prepublishOnly": "npm run build",
|
|
17
|
-
"test": "node --test dist/test",
|
|
18
|
-
"test:src": "node --import tsx --test src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts"
|
|
19
|
-
},
|
|
20
|
-
"keywords": [
|
|
21
|
-
"claude-code",
|
|
22
|
-
"statusline",
|
|
23
|
-
"cli",
|
|
24
|
-
"dashboard"
|
|
25
|
-
],
|
|
26
|
-
"license": "MIT",
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=20"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/node": "^24.0.0",
|
|
32
|
-
"tsx": "^4.20.0",
|
|
33
|
-
"typescript": "^5.8.3"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ccus-cli",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Claude Code statusline usage logger and dashboard CLI",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ccus": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/cli.js",
|
|
11
|
+
"dist/lib/**/*.js",
|
|
12
|
+
"dist/types.js"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"test": "node --test dist/test",
|
|
18
|
+
"test:src": "node --import tsx --test src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude-code",
|
|
22
|
+
"statusline",
|
|
23
|
+
"cli",
|
|
24
|
+
"dashboard"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.0.0",
|
|
32
|
+
"tsx": "^4.20.0",
|
|
33
|
+
"typescript": "^5.8.3"
|
|
34
|
+
}
|
|
35
|
+
}
|