idletime 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.
- package/README.md +272 -0
- package/assets/idle-time-readme.png +0 -0
- package/dist/idletime.js +1503 -0
- package/package.json +60 -0
package/dist/idletime.js
ADDED
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// package.json
|
|
3
|
+
var package_default = {
|
|
4
|
+
name: "idletime",
|
|
5
|
+
version: "0.1.0",
|
|
6
|
+
description: "Visual CLI for Codex focus, token burn, spikes, and idle time from local session logs.",
|
|
7
|
+
author: "ParkerRex",
|
|
8
|
+
main: "./dist/idletime.js",
|
|
9
|
+
module: "dist/idletime.js",
|
|
10
|
+
bin: {
|
|
11
|
+
idletime: "dist/idletime.js"
|
|
12
|
+
},
|
|
13
|
+
repository: {
|
|
14
|
+
type: "git",
|
|
15
|
+
url: "git+https://github.com/ParkerRex/idletime.git"
|
|
16
|
+
},
|
|
17
|
+
homepage: "https://github.com/ParkerRex/idletime#readme",
|
|
18
|
+
bugs: {
|
|
19
|
+
url: "https://github.com/ParkerRex/idletime/issues"
|
|
20
|
+
},
|
|
21
|
+
publishConfig: {
|
|
22
|
+
access: "public",
|
|
23
|
+
provenance: true
|
|
24
|
+
},
|
|
25
|
+
files: [
|
|
26
|
+
"dist",
|
|
27
|
+
"assets",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
engines: {
|
|
31
|
+
node: ">=20",
|
|
32
|
+
bun: ">=1.3.0"
|
|
33
|
+
},
|
|
34
|
+
keywords: [
|
|
35
|
+
"cli",
|
|
36
|
+
"codex",
|
|
37
|
+
"developer-tools",
|
|
38
|
+
"productivity",
|
|
39
|
+
"terminal",
|
|
40
|
+
"tokens"
|
|
41
|
+
],
|
|
42
|
+
license: "UNLICENSED",
|
|
43
|
+
packageManager: "bun@1.3.5",
|
|
44
|
+
preferGlobal: true,
|
|
45
|
+
type: "module",
|
|
46
|
+
sideEffects: false,
|
|
47
|
+
scripts: {
|
|
48
|
+
build: "bun run src/release/build-package.ts",
|
|
49
|
+
"check:release": "bun run typecheck && bun test && bun run build && npm pack --dry-run",
|
|
50
|
+
dev: "bun run src/cli/idletime-bin.ts",
|
|
51
|
+
idletime: "bun run src/cli/idletime-bin.ts",
|
|
52
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
53
|
+
"publish:dry-run": "bun run build && bun publish --dry-run --access public",
|
|
54
|
+
prepublishOnly: "bun run check:release",
|
|
55
|
+
test: "bun test",
|
|
56
|
+
typecheck: "tsc --noEmit"
|
|
57
|
+
},
|
|
58
|
+
devDependencies: {
|
|
59
|
+
"@types/bun": "latest",
|
|
60
|
+
typescript: "^5.9.3"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/report-window/parse-duration.ts
|
|
65
|
+
var durationPattern = /^(\d+)(m|h|d)$/;
|
|
66
|
+
function parseDurationToMs(durationText) {
|
|
67
|
+
const match = durationPattern.exec(durationText.trim());
|
|
68
|
+
if (!match) {
|
|
69
|
+
throw new Error(`Unsupported duration "${durationText}". Use 15m, 24h, or 2d.`);
|
|
70
|
+
}
|
|
71
|
+
const value = Number(match[1]);
|
|
72
|
+
const unit = match[2];
|
|
73
|
+
const unitMultiplier = unit === "m" ? 60000 : unit === "h" ? 3600000 : 86400000;
|
|
74
|
+
return value * unitMultiplier;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/report-window/time-zone.ts
|
|
78
|
+
var formatterCache = new Map;
|
|
79
|
+
function getZonedDateParts(timestamp, timeZone) {
|
|
80
|
+
const formatter = getFormatter(timeZone);
|
|
81
|
+
const parts = formatter.formatToParts(timestamp);
|
|
82
|
+
return {
|
|
83
|
+
year: readPart(parts, "year"),
|
|
84
|
+
month: readPart(parts, "month"),
|
|
85
|
+
day: readPart(parts, "day"),
|
|
86
|
+
hour: readPart(parts, "hour"),
|
|
87
|
+
minute: readPart(parts, "minute"),
|
|
88
|
+
second: readPart(parts, "second")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createUtcDateFromZonedParts(parts, timeZone) {
|
|
92
|
+
let candidate = new Date(Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second));
|
|
93
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
94
|
+
const actualParts = getZonedDateParts(candidate, timeZone);
|
|
95
|
+
const desiredTimestampMs = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
96
|
+
const actualTimestampMs = Date.UTC(actualParts.year, actualParts.month - 1, actualParts.day, actualParts.hour, actualParts.minute, actualParts.second);
|
|
97
|
+
const adjustmentMs = desiredTimestampMs - actualTimestampMs;
|
|
98
|
+
if (adjustmentMs === 0) {
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
candidate = new Date(candidate.getTime() + adjustmentMs);
|
|
102
|
+
}
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
function getFormatter(timeZone) {
|
|
106
|
+
const cachedFormatter = formatterCache.get(timeZone);
|
|
107
|
+
if (cachedFormatter) {
|
|
108
|
+
return cachedFormatter;
|
|
109
|
+
}
|
|
110
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
111
|
+
timeZone,
|
|
112
|
+
year: "numeric",
|
|
113
|
+
month: "2-digit",
|
|
114
|
+
day: "2-digit",
|
|
115
|
+
hour: "2-digit",
|
|
116
|
+
minute: "2-digit",
|
|
117
|
+
second: "2-digit",
|
|
118
|
+
hour12: false
|
|
119
|
+
});
|
|
120
|
+
formatterCache.set(timeZone, formatter);
|
|
121
|
+
return formatter;
|
|
122
|
+
}
|
|
123
|
+
function readPart(parts, type) {
|
|
124
|
+
const part = parts.find((entry) => entry.type === type);
|
|
125
|
+
if (!part) {
|
|
126
|
+
throw new Error(`Missing ${type} part while formatting zoned date.`);
|
|
127
|
+
}
|
|
128
|
+
return Number(part.value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/reporting/time-interval.ts
|
|
132
|
+
function mergeTimeIntervals(intervals) {
|
|
133
|
+
if (intervals.length === 0) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const sortedIntervals = [...intervals].sort((leftInterval, rightInterval) => leftInterval.start.getTime() - rightInterval.start.getTime());
|
|
137
|
+
const mergedIntervals = [copyInterval(sortedIntervals[0])];
|
|
138
|
+
for (const interval of sortedIntervals.slice(1)) {
|
|
139
|
+
const currentInterval = mergedIntervals[mergedIntervals.length - 1];
|
|
140
|
+
if (interval.start.getTime() <= currentInterval.end.getTime()) {
|
|
141
|
+
currentInterval.end = new Date(Math.max(currentInterval.end.getTime(), interval.end.getTime()));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
mergedIntervals.push(copyInterval(interval));
|
|
145
|
+
}
|
|
146
|
+
return mergedIntervals;
|
|
147
|
+
}
|
|
148
|
+
function intersectTimeIntervals(leftIntervals, rightIntervals) {
|
|
149
|
+
const intersections = [];
|
|
150
|
+
for (const leftInterval of leftIntervals) {
|
|
151
|
+
for (const rightInterval of rightIntervals) {
|
|
152
|
+
const start = new Date(Math.max(leftInterval.start.getTime(), rightInterval.start.getTime()));
|
|
153
|
+
const end = new Date(Math.min(leftInterval.end.getTime(), rightInterval.end.getTime()));
|
|
154
|
+
if (start.getTime() < end.getTime()) {
|
|
155
|
+
intersections.push({ start, end });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return mergeTimeIntervals(intersections);
|
|
160
|
+
}
|
|
161
|
+
function subtractTimeIntervals(baseIntervals, intervalsToRemove) {
|
|
162
|
+
const mergedRemovals = mergeTimeIntervals(intervalsToRemove);
|
|
163
|
+
let remainingIntervals = mergeTimeIntervals(baseIntervals);
|
|
164
|
+
for (const removal of mergedRemovals) {
|
|
165
|
+
const nextRemainingIntervals = [];
|
|
166
|
+
for (const interval of remainingIntervals) {
|
|
167
|
+
if (removal.end.getTime() <= interval.start.getTime() || removal.start.getTime() >= interval.end.getTime()) {
|
|
168
|
+
nextRemainingIntervals.push(interval);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (removal.start.getTime() > interval.start.getTime()) {
|
|
172
|
+
nextRemainingIntervals.push({
|
|
173
|
+
start: interval.start,
|
|
174
|
+
end: new Date(removal.start)
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (removal.end.getTime() < interval.end.getTime()) {
|
|
178
|
+
nextRemainingIntervals.push({
|
|
179
|
+
start: new Date(removal.end),
|
|
180
|
+
end: interval.end
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
remainingIntervals = nextRemainingIntervals;
|
|
185
|
+
}
|
|
186
|
+
return remainingIntervals;
|
|
187
|
+
}
|
|
188
|
+
function sumTimeIntervalsMs(intervals) {
|
|
189
|
+
return intervals.reduce((totalDurationMs, interval) => totalDurationMs + (interval.end.getTime() - interval.start.getTime()), 0);
|
|
190
|
+
}
|
|
191
|
+
function measureOverlapMs(intervals, targetInterval) {
|
|
192
|
+
return sumTimeIntervalsMs(intersectTimeIntervals(intervals, [targetInterval]));
|
|
193
|
+
}
|
|
194
|
+
function peakConcurrency(intervalGroups) {
|
|
195
|
+
const edges = [];
|
|
196
|
+
for (const intervalGroup of intervalGroups) {
|
|
197
|
+
for (const interval of intervalGroup) {
|
|
198
|
+
edges.push({ timestampMs: interval.start.getTime(), delta: 1 });
|
|
199
|
+
edges.push({ timestampMs: interval.end.getTime(), delta: -1 });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
edges.sort((leftEdge, rightEdge) => leftEdge.timestampMs - rightEdge.timestampMs || leftEdge.delta - rightEdge.delta);
|
|
203
|
+
let activeCount = 0;
|
|
204
|
+
let peakActiveCount = 0;
|
|
205
|
+
for (const edge of edges) {
|
|
206
|
+
activeCount += edge.delta;
|
|
207
|
+
peakActiveCount = Math.max(peakActiveCount, activeCount);
|
|
208
|
+
}
|
|
209
|
+
return peakActiveCount;
|
|
210
|
+
}
|
|
211
|
+
function copyInterval(interval) {
|
|
212
|
+
return {
|
|
213
|
+
start: new Date(interval.start),
|
|
214
|
+
end: new Date(interval.end)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/reporting/wake-window.ts
|
|
219
|
+
function parseWakeWindow(wakeWindowText) {
|
|
220
|
+
const match = /^(\d{2}):(\d{2})-(\d{2}):(\d{2})$/.exec(wakeWindowText.trim());
|
|
221
|
+
if (!match) {
|
|
222
|
+
throw new Error(`Unsupported wake window "${wakeWindowText}". Use HH:MM-HH:MM.`);
|
|
223
|
+
}
|
|
224
|
+
const startMinutes = Number(match[1]) * 60 + Number(match[2]);
|
|
225
|
+
const endMinutes = Number(match[3]) * 60 + Number(match[4]);
|
|
226
|
+
if (startMinutes > 24 * 60 || endMinutes > 24 * 60) {
|
|
227
|
+
throw new Error("Wake window minutes must stay within the day.");
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
label: wakeWindowText,
|
|
231
|
+
startMinutes,
|
|
232
|
+
endMinutes
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function summarizeWakeWindow(wakeWindow, reportWindow, activityMetrics) {
|
|
236
|
+
const wakeIntervals = buildWakeIntervalsForReportWindow(wakeWindow, reportWindow);
|
|
237
|
+
const strictIntervals = intersectTimeIntervals(activityMetrics.strictEngagementBlocks, wakeIntervals);
|
|
238
|
+
const directIntervals = intersectTimeIntervals(activityMetrics.directActivityBlocks, wakeIntervals);
|
|
239
|
+
const agentOnlyIntervals = intersectTimeIntervals(activityMetrics.agentOnlyBlocks, wakeIntervals);
|
|
240
|
+
const awakeBusyIntervals = mergeTimeIntervals([
|
|
241
|
+
...directIntervals,
|
|
242
|
+
...agentOnlyIntervals
|
|
243
|
+
]);
|
|
244
|
+
const awakeIdleIntervals = subtractTimeIntervals(wakeIntervals, awakeBusyIntervals);
|
|
245
|
+
const wakeDurationMs = sumTimeIntervalsMs(wakeIntervals);
|
|
246
|
+
const awakeIdleMs = sumTimeIntervalsMs(awakeIdleIntervals);
|
|
247
|
+
return {
|
|
248
|
+
wakeDurationMs,
|
|
249
|
+
strictEngagementMs: sumTimeIntervalsMs(strictIntervals),
|
|
250
|
+
directActivityMs: sumTimeIntervalsMs(directIntervals),
|
|
251
|
+
agentOnlyMs: sumTimeIntervalsMs(agentOnlyIntervals),
|
|
252
|
+
awakeIdleMs,
|
|
253
|
+
awakeIdlePercentage: wakeDurationMs === 0 ? 0 : awakeIdleMs / wakeDurationMs,
|
|
254
|
+
longestIdleGapMs: awakeIdleIntervals.reduce((longestGapMs, interval) => Math.max(longestGapMs, interval.end.getTime() - interval.start.getTime()), 0)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function buildWakeIntervalsForReportWindow(wakeWindow, reportWindow) {
|
|
258
|
+
const intervals = [];
|
|
259
|
+
const firstDayParts = getZonedDateParts(reportWindow.start, reportWindow.timeZone);
|
|
260
|
+
const lastDayParts = getZonedDateParts(reportWindow.end, reportWindow.timeZone);
|
|
261
|
+
const firstDay = new Date(Date.UTC(firstDayParts.year, firstDayParts.month - 1, firstDayParts.day));
|
|
262
|
+
const lastDay = new Date(Date.UTC(lastDayParts.year, lastDayParts.month - 1, lastDayParts.day));
|
|
263
|
+
for (const day = new Date(firstDay);day.getTime() <= lastDay.getTime(); day.setUTCDate(day.getUTCDate() + 1)) {
|
|
264
|
+
const start = createUtcDateFromZonedParts({
|
|
265
|
+
year: day.getUTCFullYear(),
|
|
266
|
+
month: day.getUTCMonth() + 1,
|
|
267
|
+
day: day.getUTCDate(),
|
|
268
|
+
hour: Math.floor(wakeWindow.startMinutes / 60),
|
|
269
|
+
minute: wakeWindow.startMinutes % 60,
|
|
270
|
+
second: 0
|
|
271
|
+
}, reportWindow.timeZone);
|
|
272
|
+
const endDay = new Date(day);
|
|
273
|
+
if (wakeWindow.endMinutes <= wakeWindow.startMinutes) {
|
|
274
|
+
endDay.setUTCDate(endDay.getUTCDate() + 1);
|
|
275
|
+
}
|
|
276
|
+
const end = createUtcDateFromZonedParts({
|
|
277
|
+
year: endDay.getUTCFullYear(),
|
|
278
|
+
month: endDay.getUTCMonth() + 1,
|
|
279
|
+
day: endDay.getUTCDate(),
|
|
280
|
+
hour: Math.floor(wakeWindow.endMinutes / 60),
|
|
281
|
+
minute: wakeWindow.endMinutes % 60,
|
|
282
|
+
second: 0
|
|
283
|
+
}, reportWindow.timeZone);
|
|
284
|
+
const clippedStart = new Date(Math.max(start.getTime(), reportWindow.start.getTime()));
|
|
285
|
+
const clippedEnd = new Date(Math.min(end.getTime(), reportWindow.end.getTime()));
|
|
286
|
+
if (clippedStart.getTime() < clippedEnd.getTime()) {
|
|
287
|
+
intervals.push({ start: clippedStart, end: clippedEnd });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return intervals;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/cli/parse-idletime-command.ts
|
|
294
|
+
var defaultIdleCutoffMs = parseDurationToMs("15m");
|
|
295
|
+
var defaultWindowMs = parseDurationToMs("24h");
|
|
296
|
+
function parseIdletimeCommand(argv) {
|
|
297
|
+
const args = [...argv];
|
|
298
|
+
const firstArgument = args[0];
|
|
299
|
+
const commandName = isCommandName(firstArgument) ? firstArgument : "last24h";
|
|
300
|
+
if (isCommandName(firstArgument)) {
|
|
301
|
+
args.shift();
|
|
302
|
+
}
|
|
303
|
+
const filters = {
|
|
304
|
+
workspaceOnlyPrefix: null,
|
|
305
|
+
sessionKind: null,
|
|
306
|
+
model: null,
|
|
307
|
+
reasoningEffort: null
|
|
308
|
+
};
|
|
309
|
+
const groupBy = [];
|
|
310
|
+
let helpRequested = false;
|
|
311
|
+
let hourlyWindowMs = defaultWindowMs;
|
|
312
|
+
let idleCutoffMs = defaultIdleCutoffMs;
|
|
313
|
+
let shareMode = false;
|
|
314
|
+
let versionRequested = false;
|
|
315
|
+
let wakeWindow = null;
|
|
316
|
+
while (args.length > 0) {
|
|
317
|
+
const argument = args.shift();
|
|
318
|
+
if (!argument) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
if (argument === "--help" || argument === "-h" || argument === "help") {
|
|
322
|
+
helpRequested = true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (argument === "--version" || argument === "-v") {
|
|
326
|
+
versionRequested = true;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (argument === "--window") {
|
|
330
|
+
hourlyWindowMs = parseDurationToMs(readFlagValue(argument, args));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (argument === "--idle-cutoff") {
|
|
334
|
+
idleCutoffMs = parseDurationToMs(readFlagValue(argument, args));
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (argument === "--workspace-only") {
|
|
338
|
+
filters.workspaceOnlyPrefix = readFlagValue(argument, args);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (argument === "--session-kind") {
|
|
342
|
+
filters.sessionKind = parseSessionKind(readFlagValue(argument, args));
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (argument === "--model") {
|
|
346
|
+
filters.model = readFlagValue(argument, args);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (argument === "--effort") {
|
|
350
|
+
filters.reasoningEffort = readFlagValue(argument, args);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (argument === "--wake") {
|
|
354
|
+
wakeWindow = parseWakeWindow(readFlagValue(argument, args));
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (argument === "--share") {
|
|
358
|
+
shareMode = true;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (argument === "--group-by") {
|
|
362
|
+
const dimension = readFlagValue(argument, args);
|
|
363
|
+
if (dimension !== "model" && dimension !== "effort") {
|
|
364
|
+
throw new Error(`Unsupported group-by dimension "${dimension}".`);
|
|
365
|
+
}
|
|
366
|
+
if (!groupBy.includes(dimension)) {
|
|
367
|
+
groupBy.push(dimension);
|
|
368
|
+
}
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
throw new Error(`Unknown argument "${argument}".`);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
commandName,
|
|
375
|
+
filters,
|
|
376
|
+
groupBy,
|
|
377
|
+
helpRequested,
|
|
378
|
+
hourlyWindowMs,
|
|
379
|
+
idleCutoffMs,
|
|
380
|
+
shareMode,
|
|
381
|
+
versionRequested,
|
|
382
|
+
wakeWindow
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function renderHelpText() {
|
|
386
|
+
return [
|
|
387
|
+
"idletime",
|
|
388
|
+
"Track Codex focus, activity, idle time, and token burn from local session logs.",
|
|
389
|
+
"",
|
|
390
|
+
"Usage:",
|
|
391
|
+
" idletime [last24h|today|hourly] [options]",
|
|
392
|
+
" inside this repo: bun run idletime [last24h|today|hourly] [options]",
|
|
393
|
+
"",
|
|
394
|
+
"Modes:",
|
|
395
|
+
" last24h default. visual trailing-24h dashboard with rhythm, spikes, and stats",
|
|
396
|
+
" today local-midnight-to-now summary for the current day",
|
|
397
|
+
" hourly trailing-window chart plus the detailed per-hour table",
|
|
398
|
+
"",
|
|
399
|
+
"How To Read The Dashboard:",
|
|
400
|
+
" focus strict engagement inferred from actual user_message arrivals",
|
|
401
|
+
" active broader direct-session activity in the main thread",
|
|
402
|
+
" idle awake idle time when you pass --wake",
|
|
403
|
+
" quiet non-active time when no wake window is supplied",
|
|
404
|
+
" burn practical burn = input - cached_input + output",
|
|
405
|
+
"",
|
|
406
|
+
"Options:",
|
|
407
|
+
" --window <24h> trailing window for hourly or last24h",
|
|
408
|
+
" --idle-cutoff <15m> how long activity stays live after the last event",
|
|
409
|
+
" --workspace-only <dir> include only sessions whose cwd starts with this path",
|
|
410
|
+
" --session-kind <kind> direct or subagent",
|
|
411
|
+
" --model <name> include only one primary model",
|
|
412
|
+
" --effort <level> include only one primary reasoning effort",
|
|
413
|
+
" --wake <HH:MM-HH:MM> turn quiet time into real awake idle time",
|
|
414
|
+
" --group-by <dimension> model or effort; repeatable for summaries",
|
|
415
|
+
" --share trim the output into a screenshot card",
|
|
416
|
+
" --version print the CLI version",
|
|
417
|
+
"",
|
|
418
|
+
"Examples:",
|
|
419
|
+
" idletime",
|
|
420
|
+
" idletime --wake 07:45-23:30",
|
|
421
|
+
" idletime --wake 07:45-23:30 --share",
|
|
422
|
+
" idletime today --workspace-only /path/to/demo-workspace",
|
|
423
|
+
" idletime hourly --window 24h --workspace-only /path/to/demo-workspace",
|
|
424
|
+
" idletime --version"
|
|
425
|
+
].join(`
|
|
426
|
+
`);
|
|
427
|
+
}
|
|
428
|
+
function isCommandName(value) {
|
|
429
|
+
return value === "hourly" || value === "last24h" || value === "today";
|
|
430
|
+
}
|
|
431
|
+
function parseSessionKind(sessionKindText) {
|
|
432
|
+
if (sessionKindText === "direct" || sessionKindText === "subagent") {
|
|
433
|
+
return sessionKindText;
|
|
434
|
+
}
|
|
435
|
+
throw new Error(`Unsupported session kind "${sessionKindText}".`);
|
|
436
|
+
}
|
|
437
|
+
function readFlagValue(flagName, args) {
|
|
438
|
+
const value = args.shift();
|
|
439
|
+
if (!value) {
|
|
440
|
+
throw new Error(`${flagName} requires a value.`);
|
|
441
|
+
}
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/codex-session-log/read-codex-sessions.ts
|
|
446
|
+
import { readdir } from "node:fs/promises";
|
|
447
|
+
import { homedir } from "node:os";
|
|
448
|
+
import { join } from "node:path";
|
|
449
|
+
|
|
450
|
+
// src/codex-session-log/parse-codex-session.ts
|
|
451
|
+
import { readFile } from "node:fs/promises";
|
|
452
|
+
|
|
453
|
+
// src/codex-session-log/codex-log-values.ts
|
|
454
|
+
function expectObject(value, label) {
|
|
455
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
456
|
+
throw new Error(`${label} must be an object.`);
|
|
457
|
+
}
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
function readString(record, key, label) {
|
|
461
|
+
const value = record[key];
|
|
462
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
463
|
+
throw new Error(`${label}.${key} must be a non-empty string.`);
|
|
464
|
+
}
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
function readOptionalString(record, key) {
|
|
468
|
+
const value = record[key];
|
|
469
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
470
|
+
}
|
|
471
|
+
function readNumber(record, key, label) {
|
|
472
|
+
const value = record[key];
|
|
473
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
474
|
+
throw new Error(`${label}.${key} must be a number.`);
|
|
475
|
+
}
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
function readIsoTimestamp(value, label) {
|
|
479
|
+
if (typeof value !== "string") {
|
|
480
|
+
throw new Error(`${label} must be an ISO timestamp string.`);
|
|
481
|
+
}
|
|
482
|
+
const timestamp = new Date(value);
|
|
483
|
+
if (Number.isNaN(timestamp.getTime())) {
|
|
484
|
+
throw new Error(`${label} is not a valid ISO timestamp.`);
|
|
485
|
+
}
|
|
486
|
+
return timestamp;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/codex-session-log/classify-session-kind.ts
|
|
490
|
+
function classifySessionKind(sourceValue) {
|
|
491
|
+
if (sourceValue === undefined || sourceValue === "cli" || sourceValue === "exec" || sourceValue === "vscode") {
|
|
492
|
+
return "direct";
|
|
493
|
+
}
|
|
494
|
+
const sourceRecord = expectObject(sourceValue, "session_meta.payload.source");
|
|
495
|
+
if ("subagent" in sourceRecord) {
|
|
496
|
+
return "subagent";
|
|
497
|
+
}
|
|
498
|
+
if ("custom" in sourceRecord) {
|
|
499
|
+
return "direct";
|
|
500
|
+
}
|
|
501
|
+
throw new Error("Unsupported session source payload.");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/codex-session-log/codex-log-line.ts
|
|
505
|
+
function parseCodexLogLine(lineText, sourceFilePath, lineNumber) {
|
|
506
|
+
let parsedValue;
|
|
507
|
+
try {
|
|
508
|
+
parsedValue = JSON.parse(lineText);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
throw new Error(`Failed to parse JSON in ${sourceFilePath}:${lineNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
511
|
+
}
|
|
512
|
+
const record = expectObject(parsedValue, `${sourceFilePath}:${lineNumber}`);
|
|
513
|
+
return {
|
|
514
|
+
timestamp: readIsoTimestamp(record.timestamp, `${sourceFilePath}:${lineNumber}.timestamp`),
|
|
515
|
+
type: readString(record, "type", `${sourceFilePath}:${lineNumber}`),
|
|
516
|
+
payload: record.payload
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/codex-session-log/token-usage.ts
|
|
521
|
+
function readTokenUsage(value, label) {
|
|
522
|
+
const record = expectObject(value, label);
|
|
523
|
+
const inputTokens = readNumber(record, "input_tokens", label);
|
|
524
|
+
const cachedInputTokens = readNumber(record, "cached_input_tokens", label);
|
|
525
|
+
const outputTokens = readNumber(record, "output_tokens", label);
|
|
526
|
+
const reasoningOutputTokens = readNumber(record, "reasoning_output_tokens", label);
|
|
527
|
+
const totalTokens = readNumber(record, "total_tokens", label);
|
|
528
|
+
return {
|
|
529
|
+
inputTokens,
|
|
530
|
+
cachedInputTokens,
|
|
531
|
+
outputTokens,
|
|
532
|
+
reasoningOutputTokens,
|
|
533
|
+
totalTokens,
|
|
534
|
+
practicalBurn: inputTokens - cachedInputTokens + outputTokens
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function subtractTokenUsages(currentUsage, previousUsage) {
|
|
538
|
+
const nextUsage = {
|
|
539
|
+
inputTokens: currentUsage.inputTokens - previousUsage.inputTokens,
|
|
540
|
+
cachedInputTokens: currentUsage.cachedInputTokens - previousUsage.cachedInputTokens,
|
|
541
|
+
outputTokens: currentUsage.outputTokens - previousUsage.outputTokens,
|
|
542
|
+
reasoningOutputTokens: currentUsage.reasoningOutputTokens - previousUsage.reasoningOutputTokens,
|
|
543
|
+
totalTokens: currentUsage.totalTokens - previousUsage.totalTokens,
|
|
544
|
+
practicalBurn: currentUsage.practicalBurn - previousUsage.practicalBurn
|
|
545
|
+
};
|
|
546
|
+
for (const [key, value] of Object.entries(nextUsage)) {
|
|
547
|
+
if (value < 0) {
|
|
548
|
+
throw new Error(`Token usage regressed for ${key}.`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return nextUsage;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/codex-session-log/extract-token-points.ts
|
|
555
|
+
function extractTokenPoints(records) {
|
|
556
|
+
const tokenPoints = [];
|
|
557
|
+
for (const record of records) {
|
|
558
|
+
if (record.type !== "event_msg") {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const payload = expectObject(record.payload, "event_msg.payload");
|
|
562
|
+
if (readOptionalString(payload, "type") !== "token_count") {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const info = payload.info;
|
|
566
|
+
if (info === null || info === undefined) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const infoRecord = expectObject(info, "event_msg.payload.info");
|
|
570
|
+
tokenPoints.push({
|
|
571
|
+
timestamp: record.timestamp,
|
|
572
|
+
usage: readTokenUsage(infoRecord.total_token_usage, "event_msg.payload.info.total_token_usage")
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return tokenPoints.sort((leftPoint, rightPoint) => leftPoint.timestamp.getTime() - rightPoint.timestamp.getTime());
|
|
576
|
+
}
|
|
577
|
+
function buildTokenDeltaPoints(tokenPoints) {
|
|
578
|
+
const deltaPoints = [];
|
|
579
|
+
let previousPoint = null;
|
|
580
|
+
for (const tokenPoint of tokenPoints) {
|
|
581
|
+
deltaPoints.push({
|
|
582
|
+
timestamp: tokenPoint.timestamp,
|
|
583
|
+
cumulativeUsage: tokenPoint.usage,
|
|
584
|
+
deltaUsage: previousPoint ? subtractTokenUsages(tokenPoint.usage, previousPoint.usage) : tokenPoint.usage
|
|
585
|
+
});
|
|
586
|
+
previousPoint = tokenPoint;
|
|
587
|
+
}
|
|
588
|
+
return deltaPoints;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/codex-session-log/extract-turn-attribution.ts
|
|
592
|
+
function extractTurnAttribution(records) {
|
|
593
|
+
const agentSpawnRequests = [];
|
|
594
|
+
const turnAttributions = [];
|
|
595
|
+
for (const record of records) {
|
|
596
|
+
if (record.type === "turn_context") {
|
|
597
|
+
const payload2 = expectObject(record.payload, "turn_context.payload");
|
|
598
|
+
turnAttributions.push({
|
|
599
|
+
turnId: readString(payload2, "turn_id", "turn_context.payload"),
|
|
600
|
+
timestamp: record.timestamp,
|
|
601
|
+
cwd: readString(payload2, "cwd", "turn_context.payload"),
|
|
602
|
+
model: readOptionalString(payload2, "model"),
|
|
603
|
+
reasoningEffort: readOptionalString(payload2, "effort")
|
|
604
|
+
});
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (record.type !== "response_item") {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const payload = expectObject(record.payload, "response_item.payload");
|
|
611
|
+
if (readOptionalString(payload, "type") !== "function_call" || readOptionalString(payload, "name") !== "spawn_agent") {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const argumentsText = readString(payload, "arguments", "response_item.payload");
|
|
615
|
+
let parsedArguments;
|
|
616
|
+
try {
|
|
617
|
+
parsedArguments = JSON.parse(argumentsText);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
throw new Error(`spawn_agent arguments are not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
620
|
+
}
|
|
621
|
+
const argumentsRecord = expectObject(parsedArguments, "spawn_agent.arguments");
|
|
622
|
+
agentSpawnRequests.push({
|
|
623
|
+
timestamp: record.timestamp,
|
|
624
|
+
agentType: readOptionalString(argumentsRecord, "agent_type"),
|
|
625
|
+
model: readOptionalString(argumentsRecord, "model"),
|
|
626
|
+
reasoningEffort: readOptionalString(argumentsRecord, "reasoning_effort")
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
agentSpawnRequests,
|
|
631
|
+
turnAttributions
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function resolvePrimaryModel(turnAttributions) {
|
|
635
|
+
for (let index = turnAttributions.length - 1;index >= 0; index -= 1) {
|
|
636
|
+
const model = turnAttributions[index]?.model;
|
|
637
|
+
if (model) {
|
|
638
|
+
return model;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
function resolvePrimaryReasoningEffort(turnAttributions) {
|
|
644
|
+
for (let index = turnAttributions.length - 1;index >= 0; index -= 1) {
|
|
645
|
+
const reasoningEffort = turnAttributions[index]?.reasoningEffort;
|
|
646
|
+
if (reasoningEffort) {
|
|
647
|
+
return reasoningEffort;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/codex-session-log/extract-user-message-timestamps.ts
|
|
654
|
+
function extractUserMessageTimestamps(records) {
|
|
655
|
+
const timestamps = [];
|
|
656
|
+
for (const record of records) {
|
|
657
|
+
if (record.type !== "event_msg") {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const payload = expectObject(record.payload, "event_msg.payload");
|
|
661
|
+
if (readOptionalString(payload, "type") === "user_message") {
|
|
662
|
+
timestamps.push(record.timestamp);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return timestamps;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/codex-session-log/parse-codex-session.ts
|
|
669
|
+
async function parseCodexSession(sourceFilePath) {
|
|
670
|
+
const rawFileText = await readFile(sourceFilePath, "utf8");
|
|
671
|
+
const lineTexts = rawFileText.split(`
|
|
672
|
+
`).map((lineText) => lineText.trim()).filter((lineText) => lineText.length > 0);
|
|
673
|
+
if (lineTexts.length === 0) {
|
|
674
|
+
throw new Error(`${sourceFilePath} is empty.`);
|
|
675
|
+
}
|
|
676
|
+
const records = lineTexts.map((lineText, index) => parseCodexLogLine(lineText, sourceFilePath, index + 1));
|
|
677
|
+
const sessionMetaRecord = records[0];
|
|
678
|
+
if (!sessionMetaRecord || sessionMetaRecord.type !== "session_meta") {
|
|
679
|
+
throw new Error(`${sourceFilePath} must start with a session_meta record.`);
|
|
680
|
+
}
|
|
681
|
+
const sessionMetaPayload = expectObject(sessionMetaRecord.payload, "session_meta.payload");
|
|
682
|
+
const firstRecord = records[0];
|
|
683
|
+
const lastRecord = records[records.length - 1];
|
|
684
|
+
const tokenPoints = extractTokenPoints(records);
|
|
685
|
+
const userMessageTimestamps = extractUserMessageTimestamps(records);
|
|
686
|
+
const { turnAttributions, agentSpawnRequests } = extractTurnAttribution(records);
|
|
687
|
+
const eventTimestamps = records.filter((record) => record.type !== "session_meta").map((record) => record.timestamp);
|
|
688
|
+
return {
|
|
689
|
+
sessionId: readString(sessionMetaPayload, "id", "session_meta.payload"),
|
|
690
|
+
sourceFilePath,
|
|
691
|
+
cwd: readString(sessionMetaPayload, "cwd", "session_meta.payload"),
|
|
692
|
+
kind: classifySessionKind(sessionMetaPayload.source),
|
|
693
|
+
forkedFromSessionId: readOptionalString(sessionMetaPayload, "forked_from_id"),
|
|
694
|
+
firstTimestamp: firstRecord.timestamp,
|
|
695
|
+
lastTimestamp: lastRecord.timestamp,
|
|
696
|
+
eventTimestamps: eventTimestamps.length > 0 ? eventTimestamps : [firstRecord.timestamp],
|
|
697
|
+
tokenPoints,
|
|
698
|
+
finalTokenUsage: tokenPoints.length > 0 ? tokenPoints[tokenPoints.length - 1]?.usage ?? null : null,
|
|
699
|
+
userMessageTimestamps,
|
|
700
|
+
turnAttributions,
|
|
701
|
+
agentSpawnRequests,
|
|
702
|
+
primaryModel: resolvePrimaryModel(turnAttributions),
|
|
703
|
+
primaryReasoningEffort: resolvePrimaryReasoningEffort(turnAttributions)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/codex-session-log/read-codex-sessions.ts
|
|
708
|
+
var defaultSessionRootDirectory = join(homedir(), ".codex", "sessions");
|
|
709
|
+
async function readCodexSessions(options) {
|
|
710
|
+
if (options.windowEnd.getTime() < options.windowStart.getTime()) {
|
|
711
|
+
throw new Error("windowEnd must be later than windowStart.");
|
|
712
|
+
}
|
|
713
|
+
const sessionRootDirectory = options.sessionRootDirectory ?? defaultSessionRootDirectory;
|
|
714
|
+
const candidateFiles = await listCandidateSessionFiles(sessionRootDirectory, options.windowStart, options.windowEnd);
|
|
715
|
+
const parsedSessions = await Promise.all(candidateFiles.map((candidateFile) => parseCodexSession(candidateFile)));
|
|
716
|
+
return parsedSessions.filter((parsedSession) => parsedSession.lastTimestamp.getTime() >= options.windowStart.getTime() && parsedSession.firstTimestamp.getTime() <= options.windowEnd.getTime()).sort((leftSession, rightSession) => leftSession.firstTimestamp.getTime() - rightSession.firstTimestamp.getTime());
|
|
717
|
+
}
|
|
718
|
+
async function listCandidateSessionFiles(sessionRootDirectory, windowStart, windowEnd) {
|
|
719
|
+
const candidateDirectories = buildCandidateDirectories(sessionRootDirectory, windowStart, windowEnd);
|
|
720
|
+
const candidateFiles = [];
|
|
721
|
+
for (const candidateDirectory of candidateDirectories) {
|
|
722
|
+
const directoryEntries = await readDirectoryEntries(candidateDirectory);
|
|
723
|
+
for (const directoryEntry of directoryEntries) {
|
|
724
|
+
if (directoryEntry.isFile() && directoryEntry.name.endsWith(".jsonl")) {
|
|
725
|
+
candidateFiles.push(join(candidateDirectory, directoryEntry.name));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return candidateFiles.sort();
|
|
730
|
+
}
|
|
731
|
+
function buildCandidateDirectories(sessionRootDirectory, windowStart, windowEnd) {
|
|
732
|
+
const firstDirectoryDate = startOfLocalDay(new Date(windowStart.getTime() - 24 * 60 * 60 * 1000));
|
|
733
|
+
const lastDirectoryDate = startOfLocalDay(windowEnd);
|
|
734
|
+
const directories = [];
|
|
735
|
+
for (const currentDate = new Date(firstDirectoryDate);currentDate.getTime() <= lastDirectoryDate.getTime(); currentDate.setDate(currentDate.getDate() + 1)) {
|
|
736
|
+
directories.push(join(sessionRootDirectory, currentDate.getFullYear().toString().padStart(4, "0"), `${currentDate.getMonth() + 1}`.padStart(2, "0"), `${currentDate.getDate()}`.padStart(2, "0")));
|
|
737
|
+
}
|
|
738
|
+
return directories;
|
|
739
|
+
}
|
|
740
|
+
async function readDirectoryEntries(directoryPath) {
|
|
741
|
+
try {
|
|
742
|
+
return await readdir(directoryPath, { withFileTypes: true });
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (isMissingDirectoryError(error)) {
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function isMissingDirectoryError(error) {
|
|
751
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
752
|
+
}
|
|
753
|
+
function startOfLocalDay(timestamp) {
|
|
754
|
+
return new Date(timestamp.getFullYear(), timestamp.getMonth(), timestamp.getDate(), 0, 0, 0, 0);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/report-window/resolve-report-window.ts
|
|
758
|
+
function resolveTrailingReportWindow(options = {}) {
|
|
759
|
+
const now = options.now ?? new Date;
|
|
760
|
+
const durationMs = options.durationMs ?? 24 * 60 * 60 * 1000;
|
|
761
|
+
return {
|
|
762
|
+
label: `last${Math.round(durationMs / 3600000)}h`,
|
|
763
|
+
start: new Date(now.getTime() - durationMs),
|
|
764
|
+
end: now,
|
|
765
|
+
timeZone: options.timeZone ?? getLocalTimeZone()
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function resolveTodayReportWindow(options = {}) {
|
|
769
|
+
const now = options.now ?? new Date;
|
|
770
|
+
return {
|
|
771
|
+
label: "today",
|
|
772
|
+
start: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0),
|
|
773
|
+
end: now,
|
|
774
|
+
timeZone: options.timeZone ?? getLocalTimeZone()
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function getLocalTimeZone() {
|
|
778
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/reporting/build-activity-blocks.ts
|
|
782
|
+
function buildActivityBlocks(timestamps, idleCutoffMs) {
|
|
783
|
+
if (timestamps.length === 0) {
|
|
784
|
+
return [];
|
|
785
|
+
}
|
|
786
|
+
const intervals = timestamps.slice().sort((leftTimestamp, rightTimestamp) => leftTimestamp.getTime() - rightTimestamp.getTime()).map((timestamp) => ({
|
|
787
|
+
start: new Date(timestamp),
|
|
788
|
+
end: new Date(timestamp.getTime() + idleCutoffMs)
|
|
789
|
+
}));
|
|
790
|
+
return mergeTimeIntervals(intervals);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/reporting/activity-metrics.ts
|
|
794
|
+
function buildActivityMetrics(sessions, idleCutoffMs) {
|
|
795
|
+
const directSessions = sessions.filter((session) => session.kind === "direct");
|
|
796
|
+
const subagentSessions = sessions.filter((session) => session.kind === "subagent");
|
|
797
|
+
const strictEngagementBlocks = buildActivityBlocks(directSessions.flatMap((session) => session.userMessageTimestamps), idleCutoffMs);
|
|
798
|
+
const directActivityBlocks = buildActivityBlocks(directSessions.flatMap((session) => session.eventTimestamps), idleCutoffMs);
|
|
799
|
+
const perSubagentBlocks = subagentSessions.map((session) => buildActivityBlocks(session.eventTimestamps, idleCutoffMs));
|
|
800
|
+
const agentCoverageBlocks = buildActivityBlocks(subagentSessions.flatMap((session) => session.eventTimestamps), idleCutoffMs);
|
|
801
|
+
const agentOnlyBlocks = subtractTimeIntervals(agentCoverageBlocks, directActivityBlocks);
|
|
802
|
+
return {
|
|
803
|
+
strictEngagementBlocks,
|
|
804
|
+
directActivityBlocks,
|
|
805
|
+
agentCoverageBlocks,
|
|
806
|
+
agentOnlyBlocks,
|
|
807
|
+
perSubagentBlocks,
|
|
808
|
+
strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
|
|
809
|
+
directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
|
|
810
|
+
agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
|
|
811
|
+
agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
|
|
812
|
+
cumulativeAgentMs: perSubagentBlocks.reduce((totalDurationMs, sessionBlocks) => totalDurationMs + sumTimeIntervalsMs(sessionBlocks), 0),
|
|
813
|
+
peakConcurrentAgents: peakConcurrency(perSubagentBlocks)
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/reporting/filter-sessions.ts
|
|
818
|
+
function filterSessions(sessions, filters) {
|
|
819
|
+
return sessions.filter((session) => {
|
|
820
|
+
if (filters.workspaceOnlyPrefix && !session.cwd.startsWith(filters.workspaceOnlyPrefix)) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
if (filters.sessionKind && session.kind !== filters.sessionKind) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
if (filters.model && session.primaryModel !== filters.model) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
if (filters.reasoningEffort && session.primaryReasoningEffort !== filters.reasoningEffort) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
return true;
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function groupSessions(sessions, dimension) {
|
|
836
|
+
const groupedSessions = new Map;
|
|
837
|
+
for (const session of sessions) {
|
|
838
|
+
const key = dimension === "model" ? session.primaryModel ?? "unknown" : session.primaryReasoningEffort ?? "unknown";
|
|
839
|
+
const existingGroup = groupedSessions.get(key) ?? [];
|
|
840
|
+
existingGroup.push(session);
|
|
841
|
+
groupedSessions.set(key, existingGroup);
|
|
842
|
+
}
|
|
843
|
+
return [...groupedSessions.entries()].map(([key, groupedValues]) => ({ key, sessions: groupedValues })).sort((leftGroup, rightGroup) => rightGroup.sessions.length - leftGroup.sessions.length);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/reporting/build-hourly-report.ts
|
|
847
|
+
function buildHourlyReport(sessions, query) {
|
|
848
|
+
const filteredSessions = filterSessions(sessions, query.filters);
|
|
849
|
+
const metrics = buildActivityMetrics(filteredSessions, query.idleCutoffMs);
|
|
850
|
+
const wakeIntervals = query.wakeWindow ? buildWakeIntervalsForReportWindow(query.wakeWindow, query.window) : null;
|
|
851
|
+
const buckets = buildBuckets(query.window.start, query.window.end, filteredSessions, metrics, wakeIntervals);
|
|
852
|
+
return {
|
|
853
|
+
appliedFilters: query.filters,
|
|
854
|
+
buckets,
|
|
855
|
+
hasWakeWindow: query.wakeWindow !== null,
|
|
856
|
+
idleCutoffMs: query.idleCutoffMs,
|
|
857
|
+
maxValues: {
|
|
858
|
+
agentOnlyMs: Math.max(...buckets.map((bucket) => bucket.agentOnlyMs), 0),
|
|
859
|
+
directActivityMs: Math.max(...buckets.map((bucket) => bucket.directActivityMs), 0),
|
|
860
|
+
engagedMs: Math.max(...buckets.map((bucket) => bucket.engagedMs), 0),
|
|
861
|
+
practicalBurn: Math.max(...buckets.map((bucket) => bucket.practicalBurn), 0)
|
|
862
|
+
},
|
|
863
|
+
window: query.window
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function buildBuckets(windowStart, windowEnd, sessions, metrics, wakeIntervals) {
|
|
867
|
+
const buckets = [];
|
|
868
|
+
const firstBucketStart = startOfHour(windowStart);
|
|
869
|
+
for (const bucketStart = new Date(firstBucketStart);bucketStart.getTime() < windowEnd.getTime(); bucketStart.setHours(bucketStart.getHours() + 1)) {
|
|
870
|
+
const bucketEnd = new Date(Math.min(bucketStart.getTime() + 60 * 60 * 1000, windowEnd.getTime()));
|
|
871
|
+
const bucketInterval = { start: new Date(bucketStart), end: bucketEnd };
|
|
872
|
+
const directActivityMs = measureOverlapMs(metrics.directActivityBlocks, bucketInterval);
|
|
873
|
+
const agentOnlyMs = measureOverlapMs(metrics.agentOnlyBlocks, bucketInterval);
|
|
874
|
+
const awakeIdleMs = wakeIntervals ? Math.max(0, measureOverlapMs(wakeIntervals, bucketInterval) - directActivityMs - agentOnlyMs) : 0;
|
|
875
|
+
buckets.push({
|
|
876
|
+
start: new Date(bucketStart),
|
|
877
|
+
end: bucketEnd,
|
|
878
|
+
agentOnlyMs,
|
|
879
|
+
awakeIdleMs,
|
|
880
|
+
directActivityMs,
|
|
881
|
+
engagedMs: measureOverlapMs(metrics.strictEngagementBlocks, bucketInterval),
|
|
882
|
+
peakConcurrentAgents: peakConcurrency(metrics.perSubagentBlocks.map((sessionBlocks) => clipIntervals(sessionBlocks, bucketInterval))),
|
|
883
|
+
practicalBurn: sumTokenBurn(sessions, bucketInterval),
|
|
884
|
+
rawTotalTokens: sumRawTokenDeltas(sessions, bucketInterval),
|
|
885
|
+
sessionCount: countSessionsInBucket(sessions, bucketInterval)
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
return buckets;
|
|
889
|
+
}
|
|
890
|
+
function countSessionsInBucket(sessions, bucketInterval) {
|
|
891
|
+
return sessions.filter((session) => session.eventTimestamps.some((timestamp) => timestamp.getTime() >= bucketInterval.start.getTime() && timestamp.getTime() < bucketInterval.end.getTime())).length;
|
|
892
|
+
}
|
|
893
|
+
function sumTokenBurn(sessions, bucketInterval) {
|
|
894
|
+
return sessions.reduce((totalPracticalBurn, session) => {
|
|
895
|
+
const bucketBurn = buildTokenDeltaPoints(session.tokenPoints).filter((tokenDeltaPoint) => tokenDeltaPoint.timestamp.getTime() >= bucketInterval.start.getTime() && tokenDeltaPoint.timestamp.getTime() < bucketInterval.end.getTime()).reduce((bucketTotal, tokenDeltaPoint) => bucketTotal + tokenDeltaPoint.deltaUsage.practicalBurn, 0);
|
|
896
|
+
return totalPracticalBurn + bucketBurn;
|
|
897
|
+
}, 0);
|
|
898
|
+
}
|
|
899
|
+
function sumRawTokenDeltas(sessions, bucketInterval) {
|
|
900
|
+
return sessions.reduce((rawTokenTotal, session) => {
|
|
901
|
+
const bucketRawTokens = buildTokenDeltaPoints(session.tokenPoints).filter((tokenDeltaPoint) => tokenDeltaPoint.timestamp.getTime() >= bucketInterval.start.getTime() && tokenDeltaPoint.timestamp.getTime() < bucketInterval.end.getTime()).reduce((bucketTotal, tokenDeltaPoint) => bucketTotal + tokenDeltaPoint.deltaUsage.totalTokens, 0);
|
|
902
|
+
return rawTokenTotal + bucketRawTokens;
|
|
903
|
+
}, 0);
|
|
904
|
+
}
|
|
905
|
+
function clipIntervals(intervals, targetInterval) {
|
|
906
|
+
return intervals.flatMap((interval) => {
|
|
907
|
+
const clippedStart = new Date(Math.max(interval.start.getTime(), targetInterval.start.getTime()));
|
|
908
|
+
const clippedEnd = new Date(Math.min(interval.end.getTime(), targetInterval.end.getTime()));
|
|
909
|
+
if (clippedStart.getTime() >= clippedEnd.getTime()) {
|
|
910
|
+
return [];
|
|
911
|
+
}
|
|
912
|
+
return [{ start: clippedStart, end: clippedEnd }];
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function startOfHour(timestamp) {
|
|
916
|
+
return new Date(timestamp.getFullYear(), timestamp.getMonth(), timestamp.getDate(), timestamp.getHours(), 0, 0, 0);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/reporting/report-formatting.ts
|
|
920
|
+
var integerFormatter = new Intl.NumberFormat("en-US");
|
|
921
|
+
var compactIntegerFormatter = new Intl.NumberFormat("en-US", {
|
|
922
|
+
notation: "compact",
|
|
923
|
+
maximumFractionDigits: 1
|
|
924
|
+
});
|
|
925
|
+
function formatDurationHours(durationMs) {
|
|
926
|
+
const hours = durationMs / 3600000;
|
|
927
|
+
return `${hours.toFixed(1)}h`;
|
|
928
|
+
}
|
|
929
|
+
function formatDurationClock(durationMs) {
|
|
930
|
+
const totalMinutes = Math.round(durationMs / 60000);
|
|
931
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
932
|
+
const minutes = totalMinutes % 60;
|
|
933
|
+
return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
|
|
934
|
+
}
|
|
935
|
+
function formatInteger(value) {
|
|
936
|
+
return integerFormatter.format(Math.round(value));
|
|
937
|
+
}
|
|
938
|
+
function formatCompactInteger(value) {
|
|
939
|
+
return compactIntegerFormatter.format(Math.round(value));
|
|
940
|
+
}
|
|
941
|
+
function formatPercentage(value) {
|
|
942
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
943
|
+
}
|
|
944
|
+
function formatSignedDurationHours(durationMs) {
|
|
945
|
+
const sign = durationMs >= 0 ? "+" : "-";
|
|
946
|
+
return `${sign}${formatDurationHours(Math.abs(durationMs))}`;
|
|
947
|
+
}
|
|
948
|
+
function formatTimestamp(timestamp, reportWindow) {
|
|
949
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
950
|
+
timeZone: reportWindow.timeZone,
|
|
951
|
+
month: "2-digit",
|
|
952
|
+
day: "2-digit",
|
|
953
|
+
hour: "2-digit",
|
|
954
|
+
minute: "2-digit",
|
|
955
|
+
hour12: false
|
|
956
|
+
}).format(timestamp);
|
|
957
|
+
}
|
|
958
|
+
function formatTimeRange(startTimestamp, endTimestamp, reportWindow) {
|
|
959
|
+
return `${formatTimestamp(startTimestamp, reportWindow)} -> ${formatTimestamp(endTimestamp, reportWindow)}`;
|
|
960
|
+
}
|
|
961
|
+
function buildBar(value, maxValue, width, filledCharacter = "█", emptyCharacter = "·") {
|
|
962
|
+
if (maxValue <= 0 || value <= 0) {
|
|
963
|
+
return emptyCharacter.repeat(width);
|
|
964
|
+
}
|
|
965
|
+
const filledCount = Math.max(1, Math.round(value / maxValue * width));
|
|
966
|
+
return `${filledCharacter.repeat(Math.min(width, filledCount))}${emptyCharacter.repeat(Math.max(0, width - filledCount))}`;
|
|
967
|
+
}
|
|
968
|
+
function buildSplitBar(segments, width, emptyCharacter = "·") {
|
|
969
|
+
const totalValue = segments.reduce((currentTotal, segment) => currentTotal + Math.max(0, segment.value), 0);
|
|
970
|
+
if (totalValue <= 0) {
|
|
971
|
+
return emptyCharacter.repeat(width);
|
|
972
|
+
}
|
|
973
|
+
let remainingWidth = width;
|
|
974
|
+
let builtBar = "";
|
|
975
|
+
for (const [index, segment] of segments.entries()) {
|
|
976
|
+
const segmentWidth = index === segments.length - 1 ? remainingWidth : Math.min(remainingWidth, Math.round(Math.max(0, segment.value) / totalValue * width));
|
|
977
|
+
builtBar += segment.filledCharacter.repeat(segmentWidth);
|
|
978
|
+
remainingWidth -= segmentWidth;
|
|
979
|
+
}
|
|
980
|
+
return `${builtBar}${emptyCharacter.repeat(Math.max(0, remainingWidth))}`;
|
|
981
|
+
}
|
|
982
|
+
function formatDurationCompact(durationMs) {
|
|
983
|
+
const totalMinutes = Math.round(durationMs / 60000);
|
|
984
|
+
if (totalMinutes >= 60) {
|
|
985
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
986
|
+
const minutes = totalMinutes % 60;
|
|
987
|
+
return `${hours}h${minutes.toString().padStart(2, "0")}`;
|
|
988
|
+
}
|
|
989
|
+
return `${totalMinutes}m`;
|
|
990
|
+
}
|
|
991
|
+
function formatHourBucketLabel(timestamp, reportWindow) {
|
|
992
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
993
|
+
timeZone: reportWindow.timeZone,
|
|
994
|
+
month: "2-digit",
|
|
995
|
+
day: "2-digit",
|
|
996
|
+
hour: "2-digit",
|
|
997
|
+
hour12: false
|
|
998
|
+
}).format(timestamp);
|
|
999
|
+
}
|
|
1000
|
+
function formatHourOfDay(timestamp, reportWindow) {
|
|
1001
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
1002
|
+
timeZone: reportWindow.timeZone,
|
|
1003
|
+
hour: "2-digit",
|
|
1004
|
+
hour12: false
|
|
1005
|
+
}).format(timestamp);
|
|
1006
|
+
}
|
|
1007
|
+
function buildSparkline(values) {
|
|
1008
|
+
const levels = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
1009
|
+
const maxValue = Math.max(...values, 0);
|
|
1010
|
+
if (maxValue <= 0) {
|
|
1011
|
+
return "·".repeat(values.length);
|
|
1012
|
+
}
|
|
1013
|
+
return values.map((value) => {
|
|
1014
|
+
if (value <= 0) {
|
|
1015
|
+
return "·";
|
|
1016
|
+
}
|
|
1017
|
+
const levelIndex = Math.max(0, Math.round(value / maxValue * (levels.length - 1)));
|
|
1018
|
+
return levels[levelIndex];
|
|
1019
|
+
}).join("");
|
|
1020
|
+
}
|
|
1021
|
+
function padRight(text, width) {
|
|
1022
|
+
return `${text}${" ".repeat(Math.max(0, width - text.length))}`;
|
|
1023
|
+
}
|
|
1024
|
+
function shortenPath(pathText, maxLength) {
|
|
1025
|
+
if (pathText.length <= maxLength) {
|
|
1026
|
+
return pathText;
|
|
1027
|
+
}
|
|
1028
|
+
const pathSegments = pathText.split("/").filter((segment) => segment.length > 0);
|
|
1029
|
+
let shortenedPath = "";
|
|
1030
|
+
for (let index = pathSegments.length - 1;index >= 0; index -= 1) {
|
|
1031
|
+
const nextPath = `/${pathSegments[index]}${shortenedPath}`;
|
|
1032
|
+
if (nextPath.length + 4 > maxLength) {
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
shortenedPath = nextPath;
|
|
1036
|
+
}
|
|
1037
|
+
return `...${shortenedPath || pathText.slice(-(maxLength - 3))}`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/reporting/render-theme.ts
|
|
1041
|
+
var roleStyles = {
|
|
1042
|
+
focus: "1;38;2;56;189;248",
|
|
1043
|
+
active: "1;38;2;96;165;250",
|
|
1044
|
+
agent: "1;38;2;168;85;247",
|
|
1045
|
+
idle: "1;38;2;250;204;21",
|
|
1046
|
+
burn: "1;38;2;251;146;60",
|
|
1047
|
+
frame: "1;38;2;125;211;252",
|
|
1048
|
+
heading: "1;38;2;248;250;252",
|
|
1049
|
+
muted: "38;2;148;163;184",
|
|
1050
|
+
value: "1;38;2;226;232;240"
|
|
1051
|
+
};
|
|
1052
|
+
function createRenderOptions(shareMode) {
|
|
1053
|
+
return {
|
|
1054
|
+
colorEnabled: Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined,
|
|
1055
|
+
shareMode
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function paint(text, role, options) {
|
|
1059
|
+
if (!options.colorEnabled || text.length === 0) {
|
|
1060
|
+
return text;
|
|
1061
|
+
}
|
|
1062
|
+
return `\x1B[${roleStyles[role]}m${text}\x1B[0m`;
|
|
1063
|
+
}
|
|
1064
|
+
function dim(text, options) {
|
|
1065
|
+
if (!options.colorEnabled || text.length === 0) {
|
|
1066
|
+
return text;
|
|
1067
|
+
}
|
|
1068
|
+
return `\x1B[2m${text}\x1B[0m`;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/reporting/render-layout.ts
|
|
1072
|
+
function buildPanel(title, lines) {
|
|
1073
|
+
const innerWidth = Math.max(56, title.length + 4, ...lines.map((line) => line.length));
|
|
1074
|
+
const topRuleWidth = Math.max(0, innerWidth - title.length - 2);
|
|
1075
|
+
return [
|
|
1076
|
+
`╭─ ${title} ${"─".repeat(topRuleWidth)}╮`,
|
|
1077
|
+
...lines.map((line) => `│ ${line}${" ".repeat(innerWidth - line.length)} │`),
|
|
1078
|
+
`╰${"─".repeat(innerWidth + 2)}╯`
|
|
1079
|
+
];
|
|
1080
|
+
}
|
|
1081
|
+
function buildSectionTitle(title) {
|
|
1082
|
+
return [title, `${"─".repeat(title.length)}`];
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/reporting/render-shared-sections.ts
|
|
1086
|
+
function renderPanel(title, lines, options) {
|
|
1087
|
+
return buildPanel(title, lines).map((line) => paint(line, "frame", options));
|
|
1088
|
+
}
|
|
1089
|
+
function renderSectionTitle(title, options) {
|
|
1090
|
+
const [headingLine, ruleLine] = buildSectionTitle(title);
|
|
1091
|
+
return [
|
|
1092
|
+
paint(headingLine ?? title, "heading", options),
|
|
1093
|
+
dim(ruleLine ?? "", options)
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/reporting/render-rhythm-section.ts
|
|
1098
|
+
function buildRhythmSection(report, options) {
|
|
1099
|
+
const quietValues = report.buckets.map((bucket) => Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs));
|
|
1100
|
+
const idleValues = report.hasWakeWindow ? report.buckets.map((bucket) => bucket.awakeIdleMs) : quietValues;
|
|
1101
|
+
const idleLabel = report.hasWakeWindow ? "idle" : "quiet";
|
|
1102
|
+
return [
|
|
1103
|
+
...renderSectionTitle("24h Rhythm", options),
|
|
1104
|
+
paint(` hours ${buildHourMarkerLine(report)}`, "muted", options),
|
|
1105
|
+
renderRhythmRow("focus", buildSparkline(report.buckets.map((bucket) => bucket.engagedMs)), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.engagedMs, 0)), "focus", options),
|
|
1106
|
+
renderRhythmRow("active", buildSparkline(report.buckets.map((bucket) => bucket.directActivityMs)), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.directActivityMs, 0)), "active", options),
|
|
1107
|
+
renderRhythmRow(padRight(idleLabel, 6).trimEnd(), buildSparkline(idleValues), formatDurationCompact(idleValues.reduce((totalDurationMs, idleDurationMs) => totalDurationMs + idleDurationMs, 0)), "idle", options),
|
|
1108
|
+
renderRhythmRow("burn", buildSparkline(report.buckets.map((bucket) => bucket.practicalBurn)), formatCompactInteger(report.buckets.reduce((totalBurn, bucket) => totalBurn + bucket.practicalBurn, 0)), "burn", options)
|
|
1109
|
+
];
|
|
1110
|
+
}
|
|
1111
|
+
function buildHourMarkerLine(report) {
|
|
1112
|
+
const markerCharacters = Array.from({ length: report.buckets.length }, () => " ");
|
|
1113
|
+
for (const [index, bucket] of report.buckets.entries()) {
|
|
1114
|
+
if (index % 4 !== 0) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const hourLabel = formatHourOfDay(bucket.start, report.window);
|
|
1118
|
+
markerCharacters[index] = hourLabel[0] ?? " ";
|
|
1119
|
+
if (index + 1 < markerCharacters.length) {
|
|
1120
|
+
markerCharacters[index + 1] = hourLabel[1] ?? " ";
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return markerCharacters.join("");
|
|
1124
|
+
}
|
|
1125
|
+
function renderRhythmRow(label, sparkline, totalText, role, options) {
|
|
1126
|
+
return `${paint(` ${padRight(label, 6)}`, role, options)} ${paint(sparkline, role, options)} ${paint(totalText, "value", options)}`;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/reporting/render-spike-section.ts
|
|
1130
|
+
function buildSpikeSection(report, options) {
|
|
1131
|
+
const spikeBuckets = [...report.buckets].filter((bucket) => bucket.practicalBurn > 0).sort((leftBucket, rightBucket) => rightBucket.practicalBurn - leftBucket.practicalBurn).slice(0, 3);
|
|
1132
|
+
if (spikeBuckets.length === 0) {
|
|
1133
|
+
return [];
|
|
1134
|
+
}
|
|
1135
|
+
return [
|
|
1136
|
+
...renderSectionTitle("Spike Callouts", options),
|
|
1137
|
+
...spikeBuckets.map((bucket, index) => `${paint(` #${index + 1}`, "burn", options)} ${paint(padRight(formatHourBucketLabel(bucket.start, report.window), 8), "value", options)} ${paint(padRight(formatCompactInteger(bucket.practicalBurn), 7), "burn", options)} burn ${paint(padRight(formatDurationCompact(bucket.directActivityMs), 5), "active", options)} direct ${paint(padRight(formatDurationCompact(bucket.engagedMs), 5), "focus", options)} focus ${paint(`${bucket.peakConcurrentAgents}`.padStart(2), "agent", options)} peak`)
|
|
1138
|
+
];
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/reporting/render-hourly-report.ts
|
|
1142
|
+
var barWidth = 10;
|
|
1143
|
+
function renderHourlyReport(report, options) {
|
|
1144
|
+
return options.shareMode ? renderShareHourlyReport(report, options) : renderFullHourlyReport(report, options);
|
|
1145
|
+
}
|
|
1146
|
+
function renderFullHourlyReport(report, options) {
|
|
1147
|
+
const lines = [];
|
|
1148
|
+
const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
|
|
1149
|
+
const peakFocusBucket = report.buckets.reduce((currentPeak, bucket) => bucket.engagedMs > currentPeak.engagedMs ? bucket : currentPeak, report.buckets[0]);
|
|
1150
|
+
lines.push(...renderPanel(`idletime hourly • ${report.window.label}`, [
|
|
1151
|
+
`${formatTimestamp(report.window.start, report.window)} -> ${formatTimestamp(report.window.end, report.window)}`,
|
|
1152
|
+
buildFilterLine(report),
|
|
1153
|
+
`peaks burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourBucketLabel(peakBurnBucket.start, report.window)} • focus ${formatDurationCompact(peakFocusBucket.engagedMs)} @ ${formatHourBucketLabel(peakFocusBucket.start, report.window)} • concurrency ${Math.max(...report.buckets.map((bucket) => bucket.peakConcurrentAgents), 0)}`
|
|
1154
|
+
], options));
|
|
1155
|
+
lines.push("");
|
|
1156
|
+
lines.push(...buildRhythmSection(report, options));
|
|
1157
|
+
lines.push("");
|
|
1158
|
+
lines.push(...buildSpikeSection(report, options));
|
|
1159
|
+
lines.push("");
|
|
1160
|
+
lines.push(...renderSectionTitle("Legend", options));
|
|
1161
|
+
lines.push(dim(" E engaged D direct A agent-only B practical burn", options));
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push(...renderSectionTitle("Hourly View", options));
|
|
1164
|
+
lines.push(dim(" hour E D A B s p", options));
|
|
1165
|
+
for (const bucket of report.buckets) {
|
|
1166
|
+
lines.push(` ${paint(padRight(formatHourBucketLabel(bucket.start, report.window), 8), "muted", options)} ${paint("E", "focus", options)} ${paint(buildBar(bucket.engagedMs, report.maxValues.engagedMs, barWidth, "█"), "focus", options)} ${paint(padRight(formatDurationCompact(bucket.engagedMs), 5), "value", options)} ${paint("D", "active", options)} ${paint(buildBar(bucket.directActivityMs, report.maxValues.directActivityMs, barWidth, "▓"), "active", options)} ${paint(padRight(formatDurationCompact(bucket.directActivityMs), 5), "value", options)} ${paint("A", "agent", options)} ${paint(buildBar(bucket.agentOnlyMs, report.maxValues.agentOnlyMs, barWidth, "▒"), "agent", options)} ${paint(padRight(formatDurationCompact(bucket.agentOnlyMs), 5), "value", options)} ${paint("B", "burn", options)} ${paint(buildBar(bucket.practicalBurn, report.maxValues.practicalBurn, barWidth, "▇"), "burn", options)} ${paint(padRight(formatCompactInteger(bucket.practicalBurn), 8), "value", options)} ${paint(bucket.sessionCount.toString().padStart(3), "value", options)} ${paint(bucket.peakConcurrentAgents.toString().padStart(3), "agent", options)}`);
|
|
1167
|
+
}
|
|
1168
|
+
return lines.join(`
|
|
1169
|
+
`);
|
|
1170
|
+
}
|
|
1171
|
+
function renderShareHourlyReport(report, options) {
|
|
1172
|
+
const lines = [];
|
|
1173
|
+
const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
|
|
1174
|
+
lines.push(...renderPanel(`idletime hourly • ${report.window.label}`, [
|
|
1175
|
+
`${formatTimestamp(report.window.start, report.window)} -> ${formatTimestamp(report.window.end, report.window)}`,
|
|
1176
|
+
buildFilterLine(report),
|
|
1177
|
+
`peak burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourBucketLabel(peakBurnBucket.start, report.window)}`
|
|
1178
|
+
], options));
|
|
1179
|
+
lines.push("");
|
|
1180
|
+
lines.push(...buildRhythmSection(report, options));
|
|
1181
|
+
lines.push("");
|
|
1182
|
+
lines.push(...buildSpikeSection(report, options));
|
|
1183
|
+
return lines.join(`
|
|
1184
|
+
`);
|
|
1185
|
+
}
|
|
1186
|
+
function buildFilterLine(report) {
|
|
1187
|
+
const filterParts = [
|
|
1188
|
+
`${Math.round(report.idleCutoffMs / 60000)}m cutoff`
|
|
1189
|
+
];
|
|
1190
|
+
if (report.appliedFilters.workspaceOnlyPrefix) {
|
|
1191
|
+
filterParts.push(`workspace ${shortenPath(report.appliedFilters.workspaceOnlyPrefix, 36)}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (report.appliedFilters.sessionKind) {
|
|
1194
|
+
filterParts.push(`kind ${report.appliedFilters.sessionKind}`);
|
|
1195
|
+
}
|
|
1196
|
+
if (report.appliedFilters.model) {
|
|
1197
|
+
filterParts.push(`model ${report.appliedFilters.model}`);
|
|
1198
|
+
}
|
|
1199
|
+
if (report.appliedFilters.reasoningEffort) {
|
|
1200
|
+
filterParts.push(`effort ${report.appliedFilters.reasoningEffort}`);
|
|
1201
|
+
}
|
|
1202
|
+
return filterParts.join(" • ");
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/cli/run-hourly-command.ts
|
|
1206
|
+
async function runHourlyCommand(command) {
|
|
1207
|
+
const window = resolveTrailingReportWindow({ durationMs: command.hourlyWindowMs });
|
|
1208
|
+
const sessions = await readCodexSessions({
|
|
1209
|
+
windowStart: window.start,
|
|
1210
|
+
windowEnd: window.end
|
|
1211
|
+
});
|
|
1212
|
+
return renderHourlyReport(buildHourlyReport(sessions, {
|
|
1213
|
+
filters: command.filters,
|
|
1214
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
1215
|
+
wakeWindow: command.wakeWindow,
|
|
1216
|
+
window
|
|
1217
|
+
}), createRenderOptions(command.shareMode));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/reporting/build-summary-report.ts
|
|
1221
|
+
function buildSummaryReport(sessions, query) {
|
|
1222
|
+
const filteredSessions = filterSessions(sessions, query.filters);
|
|
1223
|
+
const windowInterval = {
|
|
1224
|
+
start: query.window.start,
|
|
1225
|
+
end: query.window.end
|
|
1226
|
+
};
|
|
1227
|
+
const metrics = clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, query.idleCutoffMs), windowInterval);
|
|
1228
|
+
const comparisonCutoffMs = parseDurationToMs("30m");
|
|
1229
|
+
const sessionCounts = {
|
|
1230
|
+
total: filteredSessions.length,
|
|
1231
|
+
direct: filteredSessions.filter((session) => session.kind === "direct").length,
|
|
1232
|
+
subagent: filteredSessions.filter((session) => session.kind === "subagent").length
|
|
1233
|
+
};
|
|
1234
|
+
return {
|
|
1235
|
+
activityWindow: resolveActivityWindow(filteredSessions, windowInterval),
|
|
1236
|
+
appliedFilters: query.filters,
|
|
1237
|
+
comparisonCutoffMs,
|
|
1238
|
+
comparisonMetrics: query.idleCutoffMs === comparisonCutoffMs ? metrics : clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, comparisonCutoffMs), windowInterval),
|
|
1239
|
+
directTokenTotals: sumTokenTotals(filteredSessions.filter((session) => session.kind === "direct"), windowInterval),
|
|
1240
|
+
groupBreakdowns: buildGroupBreakdowns(filteredSessions, query.groupBy, query.idleCutoffMs, windowInterval),
|
|
1241
|
+
idleCutoffMs: query.idleCutoffMs,
|
|
1242
|
+
metrics,
|
|
1243
|
+
sessionCounts,
|
|
1244
|
+
tokenTotals: sumTokenTotals(filteredSessions, windowInterval),
|
|
1245
|
+
wakeSummary: query.wakeWindow ? summarizeWakeWindow(query.wakeWindow, query.window, metrics) : null,
|
|
1246
|
+
window: query.window
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function buildGroupBreakdowns(sessions, dimensions, idleCutoffMs, windowInterval) {
|
|
1250
|
+
return dimensions.map((dimension) => ({
|
|
1251
|
+
dimension,
|
|
1252
|
+
rows: groupSessions(sessions, dimension).map((groupedSessions) => buildGroupRow(groupedSessions.key, groupedSessions.sessions, idleCutoffMs, windowInterval))
|
|
1253
|
+
}));
|
|
1254
|
+
}
|
|
1255
|
+
function buildGroupRow(key, sessions, idleCutoffMs, windowInterval) {
|
|
1256
|
+
const metrics = clipActivityMetricsToWindow(buildActivityMetrics(sessions, idleCutoffMs), windowInterval);
|
|
1257
|
+
const tokenTotals = sumTokenTotals(sessions, windowInterval);
|
|
1258
|
+
return {
|
|
1259
|
+
key,
|
|
1260
|
+
sessionCount: sessions.length,
|
|
1261
|
+
directActivityMs: metrics.directActivityMs,
|
|
1262
|
+
agentCoverageMs: metrics.agentCoverageMs,
|
|
1263
|
+
cumulativeAgentMs: metrics.cumulativeAgentMs,
|
|
1264
|
+
practicalBurn: tokenTotals.practicalBurn,
|
|
1265
|
+
rawTotalTokens: tokenTotals.rawTotalTokens
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function sumTokenTotals(sessions, windowInterval) {
|
|
1269
|
+
return sessions.reduce((tokenTotals, session) => {
|
|
1270
|
+
const sessionWindowTotals = buildTokenDeltaPoints(session.tokenPoints).filter((tokenDeltaPoint) => tokenDeltaPoint.timestamp.getTime() >= windowInterval.start.getTime() && tokenDeltaPoint.timestamp.getTime() <= windowInterval.end.getTime()).reduce((sessionTotals, tokenDeltaPoint) => ({
|
|
1271
|
+
practicalBurn: sessionTotals.practicalBurn + tokenDeltaPoint.deltaUsage.practicalBurn,
|
|
1272
|
+
rawTotalTokens: sessionTotals.rawTotalTokens + tokenDeltaPoint.deltaUsage.totalTokens
|
|
1273
|
+
}), { practicalBurn: 0, rawTotalTokens: 0 });
|
|
1274
|
+
return {
|
|
1275
|
+
practicalBurn: tokenTotals.practicalBurn + sessionWindowTotals.practicalBurn,
|
|
1276
|
+
rawTotalTokens: tokenTotals.rawTotalTokens + sessionWindowTotals.rawTotalTokens
|
|
1277
|
+
};
|
|
1278
|
+
}, {
|
|
1279
|
+
practicalBurn: 0,
|
|
1280
|
+
rawTotalTokens: 0
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
function resolveActivityWindow(sessions, windowInterval) {
|
|
1284
|
+
if (sessions.length === 0) {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
const firstTimestamp = sessions.reduce((earliestTimestamp, session) => session.firstTimestamp.getTime() < earliestTimestamp.getTime() ? session.firstTimestamp : earliestTimestamp, sessions[0].firstTimestamp);
|
|
1288
|
+
const lastTimestamp = sessions.reduce((latestTimestamp, session) => session.lastTimestamp.getTime() > latestTimestamp.getTime() ? session.lastTimestamp : latestTimestamp, sessions[0].lastTimestamp);
|
|
1289
|
+
return {
|
|
1290
|
+
start: new Date(Math.max(firstTimestamp.getTime(), windowInterval.start.getTime())),
|
|
1291
|
+
end: new Date(Math.min(lastTimestamp.getTime(), windowInterval.end.getTime()))
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function clipActivityMetricsToWindow(metrics, windowInterval) {
|
|
1295
|
+
const strictEngagementBlocks = intersectTimeIntervals(metrics.strictEngagementBlocks, [windowInterval]);
|
|
1296
|
+
const directActivityBlocks = intersectTimeIntervals(metrics.directActivityBlocks, [windowInterval]);
|
|
1297
|
+
const agentCoverageBlocks = intersectTimeIntervals(metrics.agentCoverageBlocks, [windowInterval]);
|
|
1298
|
+
const agentOnlyBlocks = intersectTimeIntervals(metrics.agentOnlyBlocks, [windowInterval]);
|
|
1299
|
+
const perSubagentBlocks = metrics.perSubagentBlocks.map((sessionBlocks) => intersectTimeIntervals(sessionBlocks, [windowInterval]));
|
|
1300
|
+
return {
|
|
1301
|
+
strictEngagementBlocks,
|
|
1302
|
+
directActivityBlocks,
|
|
1303
|
+
agentCoverageBlocks,
|
|
1304
|
+
agentOnlyBlocks,
|
|
1305
|
+
perSubagentBlocks,
|
|
1306
|
+
strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
|
|
1307
|
+
directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
|
|
1308
|
+
agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
|
|
1309
|
+
agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
|
|
1310
|
+
cumulativeAgentMs: perSubagentBlocks.reduce((totalDurationMs, sessionBlocks) => totalDurationMs + sumTimeIntervalsMs(sessionBlocks), 0),
|
|
1311
|
+
peakConcurrentAgents: peakConcurrency(perSubagentBlocks)
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/reporting/render-summary-report.ts
|
|
1316
|
+
var summaryBarWidth = 18;
|
|
1317
|
+
function renderSummaryReport(report, options, hourlyReport) {
|
|
1318
|
+
return options.shareMode ? renderShareSummaryReport(report, options, hourlyReport) : renderFullSummaryReport(report, options, hourlyReport);
|
|
1319
|
+
}
|
|
1320
|
+
function renderFullSummaryReport(report, options, hourlyReport) {
|
|
1321
|
+
const lines = [];
|
|
1322
|
+
const requestedMetrics = report.metrics;
|
|
1323
|
+
const actualComparisonMetrics = report.comparisonMetrics;
|
|
1324
|
+
const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
|
|
1325
|
+
const headerLines = [
|
|
1326
|
+
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
1327
|
+
report.activityWindow ? `active ${formatTimeRange(report.activityWindow.start, report.activityWindow.end, report.window)}` : "active no matching sessions",
|
|
1328
|
+
buildHeaderMeta(report),
|
|
1329
|
+
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
1330
|
+
];
|
|
1331
|
+
if (hourlyReport) {
|
|
1332
|
+
headerLines.push(buildPeakLine(hourlyReport));
|
|
1333
|
+
}
|
|
1334
|
+
lines.push(...renderPanel(`idletime • ${report.window.label}`, headerLines, options));
|
|
1335
|
+
if (hourlyReport) {
|
|
1336
|
+
lines.push("");
|
|
1337
|
+
lines.push(...buildRhythmSection(hourlyReport, options));
|
|
1338
|
+
lines.push("");
|
|
1339
|
+
lines.push(...buildSpikeSection(hourlyReport, options));
|
|
1340
|
+
}
|
|
1341
|
+
lines.push("");
|
|
1342
|
+
lines.push(...renderSectionTitle("Activity", options));
|
|
1343
|
+
lines.push(renderMetricRow("strict", requestedMetrics.strictEngagementMs, windowDurationMs, formatDurationHours(requestedMetrics.strictEngagementMs), `${formatSignedDurationHours(actualComparisonMetrics.strictEngagementMs - requestedMetrics.strictEngagementMs)} at ${formatDurationLabel(report.comparisonCutoffMs)}`, "█", "focus", options));
|
|
1344
|
+
lines.push(renderMetricRow("direct", requestedMetrics.directActivityMs, windowDurationMs, formatDurationHours(requestedMetrics.directActivityMs), `${formatSignedDurationHours(actualComparisonMetrics.directActivityMs - requestedMetrics.directActivityMs)} at ${formatDurationLabel(report.comparisonCutoffMs)}`, "▓", "active", options));
|
|
1345
|
+
lines.push(renderMetricRow("agent live", requestedMetrics.agentCoverageMs, windowDurationMs, formatDurationHours(requestedMetrics.agentCoverageMs), "coverage", "▒", "agent", options));
|
|
1346
|
+
lines.push(renderMetricRow("agent sum", requestedMetrics.cumulativeAgentMs, Math.max(windowDurationMs, requestedMetrics.cumulativeAgentMs), formatDurationHours(requestedMetrics.cumulativeAgentMs), `peak ${requestedMetrics.peakConcurrentAgents} concurrent`, "▚", "agent", options));
|
|
1347
|
+
lines.push(`${paint(padRight(" session mix", 14), "muted", options)} ${paint(buildSplitBar([
|
|
1348
|
+
{
|
|
1349
|
+
filledCharacter: "█",
|
|
1350
|
+
value: report.sessionCounts.direct
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
filledCharacter: "▓",
|
|
1354
|
+
value: report.sessionCounts.subagent
|
|
1355
|
+
}
|
|
1356
|
+
], summaryBarWidth), "active", options)} ${paint(`${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent`, "value", options)}`);
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
lines.push(...renderSectionTitle("Tokens", options));
|
|
1359
|
+
const maxBurnValue = Math.max(report.tokenTotals.practicalBurn, report.directTokenTotals.practicalBurn);
|
|
1360
|
+
const maxRawValue = Math.max(report.tokenTotals.rawTotalTokens, report.directTokenTotals.rawTotalTokens);
|
|
1361
|
+
lines.push(renderMetricRow("practical burn", report.tokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "█", "burn", options));
|
|
1362
|
+
lines.push(renderMetricRow("all raw", report.tokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.tokenTotals.rawTotalTokens), `${formatInteger(report.tokenTotals.rawTotalTokens)} total`, "▓", "burn", options));
|
|
1363
|
+
lines.push(renderMetricRow("direct burn", report.directTokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.directTokenTotals.practicalBurn), `${formatPercentage(report.directTokenTotals.practicalBurn / report.tokenTotals.practicalBurn)} of burn`, "▒", "burn", options));
|
|
1364
|
+
lines.push(renderMetricRow("direct raw", report.directTokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.directTokenTotals.rawTotalTokens), `${formatPercentage(report.directTokenTotals.rawTotalTokens / report.tokenTotals.rawTotalTokens)} of raw`, "▚", "burn", options));
|
|
1365
|
+
if (report.wakeSummary) {
|
|
1366
|
+
lines.push("");
|
|
1367
|
+
lines.push(...renderSectionTitle("Wake Window", options));
|
|
1368
|
+
lines.push(renderMetricRow("direct awake", report.wakeSummary.directActivityMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.directActivityMs), `of ${formatDurationClock(report.wakeSummary.wakeDurationMs)} wake`, "▓", "active", options));
|
|
1369
|
+
lines.push(renderMetricRow("strict awake", report.wakeSummary.strictEngagementMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.strictEngagementMs), "engaged", "█", "focus", options));
|
|
1370
|
+
lines.push(renderMetricRow("agent awake", report.wakeSummary.agentOnlyMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.agentOnlyMs), "agent-only", "▒", "agent", options));
|
|
1371
|
+
lines.push(renderMetricRow("awake idle", report.wakeSummary.awakeIdleMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.awakeIdleMs), `${formatPercentage(report.wakeSummary.awakeIdlePercentage)} idle`, "░", "idle", options));
|
|
1372
|
+
lines.push(`${paint(padRight(" longest gap", 14), "muted", options)} ${paint(formatDurationClock(report.wakeSummary.longestIdleGapMs), "value", options)} ${dim("largest quiet stretch", options)}`);
|
|
1373
|
+
}
|
|
1374
|
+
for (const groupBreakdown of report.groupBreakdowns) {
|
|
1375
|
+
lines.push("");
|
|
1376
|
+
lines.push(...renderSectionTitle(groupBreakdown.dimension === "model" ? "Model Breakdown" : "Effort Breakdown", options));
|
|
1377
|
+
const maxBreakdownBurn = Math.max(...groupBreakdown.rows.map((row) => row.practicalBurn), 0);
|
|
1378
|
+
for (const row of groupBreakdown.rows) {
|
|
1379
|
+
lines.push(`${paint(padRight(` ${row.key}`, 20), "muted", options)} ${paint(buildBar(row.practicalBurn, maxBreakdownBurn, 14, "█"), "burn", options)} ${paint(padRight(formatCompactInteger(row.practicalBurn), 6), "value", options)} ${dim("burn", options)} ${paint(padRight(formatDurationCompact(row.directActivityMs), 5), "active", options)} ${dim("direct", options)} ${paint(padRight(formatDurationCompact(row.agentCoverageMs), 5), "agent", options)} ${dim("live", options)} ${paint(`${row.sessionCount} s`, "value", options)}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return lines.join(`
|
|
1383
|
+
`);
|
|
1384
|
+
}
|
|
1385
|
+
function renderShareSummaryReport(report, options, hourlyReport) {
|
|
1386
|
+
const lines = [];
|
|
1387
|
+
const headerLines = [
|
|
1388
|
+
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
1389
|
+
report.activityWindow ? `active ${formatTimeRange(report.activityWindow.start, report.activityWindow.end, report.window)}` : "active no matching sessions",
|
|
1390
|
+
buildHeaderMeta(report),
|
|
1391
|
+
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
1392
|
+
];
|
|
1393
|
+
if (hourlyReport) {
|
|
1394
|
+
headerLines.push(buildPeakLine(hourlyReport));
|
|
1395
|
+
}
|
|
1396
|
+
lines.push(...renderPanel(`idletime • ${report.window.label}`, headerLines, options));
|
|
1397
|
+
if (hourlyReport) {
|
|
1398
|
+
lines.push("");
|
|
1399
|
+
lines.push(...buildRhythmSection(hourlyReport, options));
|
|
1400
|
+
lines.push("");
|
|
1401
|
+
lines.push(...buildSpikeSection(hourlyReport, options));
|
|
1402
|
+
}
|
|
1403
|
+
lines.push("");
|
|
1404
|
+
lines.push(...renderSectionTitle("Snapshot", options));
|
|
1405
|
+
lines.push(renderSnapshotRow("focus", formatDurationHours(report.metrics.strictEngagementMs), "focused time", "focus", options));
|
|
1406
|
+
lines.push(renderSnapshotRow("active", formatDurationHours(report.metrics.directActivityMs), "direct-session movement", "active", options));
|
|
1407
|
+
lines.push(renderSnapshotRow(report.wakeSummary ? "idle" : "quiet", report.wakeSummary ? formatDurationClock(report.wakeSummary.awakeIdleMs) : hourlyReport ? formatDurationCompact(hourlyReport.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs), 0)) : "n/a", report.wakeSummary ? "awake idle" : "quiet hours", "idle", options));
|
|
1408
|
+
lines.push(renderSnapshotRow("burn", formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "burn", options));
|
|
1409
|
+
lines.push(renderSnapshotRow("agents", `${report.metrics.peakConcurrentAgents} peak`, `${formatDurationHours(report.metrics.cumulativeAgentMs)} cumulative`, "agent", options));
|
|
1410
|
+
lines.push(renderSnapshotRow("sessions", `${report.sessionCounts.total}`, `${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent`, "value", options));
|
|
1411
|
+
return lines.join(`
|
|
1412
|
+
`);
|
|
1413
|
+
}
|
|
1414
|
+
function formatAppliedFilters(report) {
|
|
1415
|
+
const appliedFilters = [];
|
|
1416
|
+
if (report.appliedFilters.workspaceOnlyPrefix) {
|
|
1417
|
+
appliedFilters.push(`workspace=${shortenPath(report.appliedFilters.workspaceOnlyPrefix, 48)}`);
|
|
1418
|
+
}
|
|
1419
|
+
if (report.appliedFilters.sessionKind) {
|
|
1420
|
+
appliedFilters.push(`kind=${report.appliedFilters.sessionKind}`);
|
|
1421
|
+
}
|
|
1422
|
+
if (report.appliedFilters.model) {
|
|
1423
|
+
appliedFilters.push(`model=${report.appliedFilters.model}`);
|
|
1424
|
+
}
|
|
1425
|
+
if (report.appliedFilters.reasoningEffort) {
|
|
1426
|
+
appliedFilters.push(`effort=${report.appliedFilters.reasoningEffort}`);
|
|
1427
|
+
}
|
|
1428
|
+
return appliedFilters;
|
|
1429
|
+
}
|
|
1430
|
+
function formatDurationLabel(durationMs) {
|
|
1431
|
+
return `${Math.round(durationMs / 60000)}m`;
|
|
1432
|
+
}
|
|
1433
|
+
function buildHeaderMeta(report) {
|
|
1434
|
+
return `${report.sessionCounts.total} sessions • ${report.sessionCounts.direct} direct • ${report.sessionCounts.subagent} subagent • peak ${report.metrics.peakConcurrentAgents} agents`;
|
|
1435
|
+
}
|
|
1436
|
+
function buildPeakLine(report) {
|
|
1437
|
+
const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
|
|
1438
|
+
const peakFocusBucket = report.buckets.reduce((currentPeak, bucket) => bucket.engagedMs > currentPeak.engagedMs ? bucket : currentPeak, report.buckets[0]);
|
|
1439
|
+
return `peaks burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourOfDay(peakBurnBucket.start, report.window)} • focus ${formatDurationCompact(peakFocusBucket.engagedMs)} @ ${formatHourOfDay(peakFocusBucket.start, report.window)}`;
|
|
1440
|
+
}
|
|
1441
|
+
function renderMetricRow(label, value, maxValue, primaryText, detailText, filledCharacter, role, options) {
|
|
1442
|
+
return `${paint(padRight(` ${label}`, 14), "muted", options)} ${paint(buildBar(value, maxValue, summaryBarWidth, filledCharacter), role, options)} ${paint(padRight(primaryText, 7), "value", options)} ${dim(detailText, options)}`;
|
|
1443
|
+
}
|
|
1444
|
+
function renderSnapshotRow(label, primaryText, detailText, role, options) {
|
|
1445
|
+
return `${paint(padRight(` ${label}`, 12), role, options)} ${paint(padRight(primaryText, 10), "value", options)} ${dim(detailText, options)}`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/cli/run-last24h-command.ts
|
|
1449
|
+
async function runLast24hCommand(command) {
|
|
1450
|
+
const window = resolveTrailingReportWindow({ durationMs: command.hourlyWindowMs });
|
|
1451
|
+
const sessions = await readCodexSessions({
|
|
1452
|
+
windowStart: window.start,
|
|
1453
|
+
windowEnd: window.end
|
|
1454
|
+
});
|
|
1455
|
+
const summaryReport = buildSummaryReport(sessions, {
|
|
1456
|
+
filters: command.filters,
|
|
1457
|
+
groupBy: command.groupBy,
|
|
1458
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
1459
|
+
wakeWindow: command.wakeWindow,
|
|
1460
|
+
window
|
|
1461
|
+
});
|
|
1462
|
+
const hourlyReport = buildHourlyReport(sessions, {
|
|
1463
|
+
filters: command.filters,
|
|
1464
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
1465
|
+
wakeWindow: command.wakeWindow,
|
|
1466
|
+
window
|
|
1467
|
+
});
|
|
1468
|
+
return renderSummaryReport(summaryReport, createRenderOptions(command.shareMode), hourlyReport);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// src/cli/run-today-command.ts
|
|
1472
|
+
async function runTodayCommand(command) {
|
|
1473
|
+
const window = resolveTodayReportWindow();
|
|
1474
|
+
const sessions = await readCodexSessions({
|
|
1475
|
+
windowStart: window.start,
|
|
1476
|
+
windowEnd: window.end
|
|
1477
|
+
});
|
|
1478
|
+
return renderSummaryReport(buildSummaryReport(sessions, {
|
|
1479
|
+
filters: command.filters,
|
|
1480
|
+
groupBy: command.groupBy,
|
|
1481
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
1482
|
+
wakeWindow: command.wakeWindow,
|
|
1483
|
+
window
|
|
1484
|
+
}), createRenderOptions(command.shareMode));
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/cli/run-idletime.ts
|
|
1488
|
+
async function runIdletimeCli(argv) {
|
|
1489
|
+
const command = parseIdletimeCommand(argv);
|
|
1490
|
+
if (command.helpRequested) {
|
|
1491
|
+
console.log(renderHelpText());
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (command.versionRequested) {
|
|
1495
|
+
console.log(package_default.version);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const output = command.commandName === "hourly" ? await runHourlyCommand(command) : command.commandName === "today" ? await runTodayCommand(command) : await runLast24hCommand(command);
|
|
1499
|
+
console.log(output);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// src/cli/idletime-bin.ts
|
|
1503
|
+
await runIdletimeCli(process.argv.slice(2));
|