idletime 0.1.1 → 0.1.3
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
CHANGED
|
@@ -64,6 +64,7 @@ bun run idletime
|
|
|
64
64
|
|
|
65
65
|
That shows:
|
|
66
66
|
|
|
67
|
+
- A gold `BEST` plaque in the header for your top concurrent agents, top 24-hour raw burn, and top agent-sum record
|
|
67
68
|
- A framed trailing-24h dashboard
|
|
68
69
|
- A `24h Rhythm` strip for `focus`, `active`, `quiet` or `idle`, and `burn`
|
|
69
70
|
- `Spike Callouts` for the biggest burn hours
|
|
@@ -171,6 +172,20 @@ Once published, that also works as:
|
|
|
171
172
|
idletime --version
|
|
172
173
|
```
|
|
173
174
|
|
|
175
|
+
## Record Tracking
|
|
176
|
+
|
|
177
|
+
`idletime` now keeps a local personal-best ledger under `~/.idletime/`.
|
|
178
|
+
|
|
179
|
+
- `bests-v1.json`: durable best values for the header plaque
|
|
180
|
+
- `best-events.ndjson`: append-only new-best history
|
|
181
|
+
- `near-best-notifications-v1.json`: opt-in state for “close to best” nudges
|
|
182
|
+
|
|
183
|
+
By default:
|
|
184
|
+
|
|
185
|
+
- the `BEST` plaque is always shown in the normal header
|
|
186
|
+
- genuine new-best events can trigger a local macOS notification
|
|
187
|
+
- near-best nudges are stored but disabled until you opt in by setting `nearBestEnabled` to `true`
|
|
188
|
+
|
|
174
189
|
## Validation
|
|
175
190
|
|
|
176
191
|
```bash
|
|
@@ -178,6 +193,19 @@ bun run typecheck
|
|
|
178
193
|
bun test
|
|
179
194
|
```
|
|
180
195
|
|
|
196
|
+
Release QA:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
bun run qa
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
That QA pass reads:
|
|
203
|
+
|
|
204
|
+
- `qa/data/user-journeys.csv` for installed-binary shell journeys
|
|
205
|
+
- `qa/data/coverage-matrix.csv` for required release coverage rows
|
|
206
|
+
|
|
207
|
+
It builds the package, packs the current checkout, installs the tarball into an isolated temp `BUN_INSTALL`, seeds synthetic Codex session logs, and runs the shell journeys against the installed `idletime` binary.
|
|
208
|
+
|
|
181
209
|
## Release Prep
|
|
182
210
|
|
|
183
211
|
Build the publishable CLI bundle:
|
|
@@ -192,6 +220,13 @@ Dry-run the release checks:
|
|
|
192
220
|
bun run check:release
|
|
193
221
|
```
|
|
194
222
|
|
|
223
|
+
`check:release` now runs:
|
|
224
|
+
|
|
225
|
+
- `bun run typecheck`
|
|
226
|
+
- `bun test`
|
|
227
|
+
- `bun run qa`
|
|
228
|
+
- `npm pack --dry-run`
|
|
229
|
+
|
|
195
230
|
Dry-run the Bun publish flow:
|
|
196
231
|
|
|
197
232
|
```bash
|
|
@@ -206,11 +241,15 @@ bun run pack:dry-run
|
|
|
206
241
|
|
|
207
242
|
## GitHub Release Flow
|
|
208
243
|
|
|
209
|
-
This repo now includes
|
|
244
|
+
This repo now includes:
|
|
245
|
+
|
|
246
|
+
- `.github/workflows/ci.yml` for push and pull-request release checks
|
|
247
|
+
- `.github/workflows/publish.yml` for the actual npm publish flow
|
|
210
248
|
|
|
211
249
|
What it does:
|
|
212
250
|
|
|
213
|
-
- runs on
|
|
251
|
+
- `ci.yml` runs on pushes to `dev` and `main`, plus pull requests
|
|
252
|
+
- `publish.yml` runs on manual dispatch or GitHub release publish
|
|
214
253
|
- installs Bun and Node on a GitHub-hosted runner
|
|
215
254
|
- runs `bun run check:release`
|
|
216
255
|
- publishes to npm with `npm publish --access public --provenance`
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="24" y1="20" x2="228" y2="236" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0" stop-color="#202611"/>
|
|
5
|
+
<stop offset="1" stop-color="#12160A"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="glow" x1="44" y1="42" x2="212" y2="220" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop offset="0" stop-color="#C7BC61"/>
|
|
9
|
+
<stop offset="1" stop-color="#786B2A"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="top" x1="128" y1="62" x2="128" y2="116" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop offset="0" stop-color="#F0E6AB"/>
|
|
13
|
+
<stop offset="1" stop-color="#D7C770"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<linearGradient id="left" x1="90" y1="92" x2="118" y2="196" gradientUnits="userSpaceOnUse">
|
|
16
|
+
<stop offset="0" stop-color="#D0BC58"/>
|
|
17
|
+
<stop offset="1" stop-color="#937F34"/>
|
|
18
|
+
</linearGradient>
|
|
19
|
+
<linearGradient id="right" x1="166" y1="92" x2="138" y2="198" gradientUnits="userSpaceOnUse">
|
|
20
|
+
<stop offset="0" stop-color="#BFA747"/>
|
|
21
|
+
<stop offset="1" stop-color="#6E5E22"/>
|
|
22
|
+
</linearGradient>
|
|
23
|
+
</defs>
|
|
24
|
+
<rect x="16" y="16" width="224" height="224" rx="44" fill="url(#bg)"/>
|
|
25
|
+
<rect x="26" y="26" width="204" height="204" rx="34" stroke="url(#glow)" stroke-opacity=".42" stroke-width="4"/>
|
|
26
|
+
<rect x="42" y="42" width="172" height="22" rx="11" fill="url(#glow)" fill-opacity=".34"/>
|
|
27
|
+
<path d="M128 58 184 90 128 122 72 90 128 58Z" fill="url(#top)"/>
|
|
28
|
+
<path d="M72 90 128 122V190L72 158V90Z" fill="url(#left)"/>
|
|
29
|
+
<path d="M184 90 128 122V190L184 158V90Z" fill="url(#right)"/>
|
|
30
|
+
<path d="M128 58 184 90 128 122 72 90 128 58Z" stroke="#F5EDBD" stroke-opacity=".74" stroke-width="3"/>
|
|
31
|
+
<path d="M72 90 128 122M184 90 128 122M128 122V190" stroke="#FFF5C5" stroke-opacity=".42" stroke-width="3"/>
|
|
32
|
+
<path d="M86 178h84" stroke="#FFF0AB" stroke-opacity=".24" stroke-width="6" stroke-linecap="round"/>
|
|
33
|
+
</svg>
|
|
Binary file
|
package/dist/idletime.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "idletime",
|
|
5
|
-
version: "0.1.
|
|
5
|
+
version: "0.1.3",
|
|
6
6
|
description: "Visual CLI for Codex focus, token burn, spikes, and idle time from local session logs.",
|
|
7
7
|
author: "ParkerRex",
|
|
8
8
|
main: "./dist/idletime.js",
|
|
@@ -46,12 +46,15 @@ var package_default = {
|
|
|
46
46
|
sideEffects: false,
|
|
47
47
|
scripts: {
|
|
48
48
|
build: "bun run src/release/build-package.ts",
|
|
49
|
-
"check:release": "bun run typecheck && bun test && bun run
|
|
49
|
+
"check:release": "bun run build && bun run typecheck && bun test && bun run qa && npm pack --dry-run",
|
|
50
50
|
dev: "bun run src/cli/idletime-bin.ts",
|
|
51
51
|
idletime: "bun run src/cli/idletime-bin.ts",
|
|
52
|
-
"pack:dry-run": "npm pack --dry-run",
|
|
52
|
+
"pack:dry-run": "bun run build && npm pack --dry-run",
|
|
53
53
|
"publish:dry-run": "bun run build && bun publish --dry-run --access public",
|
|
54
54
|
prepublishOnly: "bun run check:release",
|
|
55
|
+
qa: "bun run qa:gaps && bun run qa:journeys",
|
|
56
|
+
"qa:gaps": "bun run qa/find-gaps.ts",
|
|
57
|
+
"qa:journeys": "bun run qa/run-shell-journeys.ts",
|
|
55
58
|
test: "bun test",
|
|
56
59
|
typecheck: "tsc --noEmit"
|
|
57
60
|
},
|
|
@@ -518,6 +521,16 @@ function parseCodexLogLine(lineText, sourceFilePath, lineNumber) {
|
|
|
518
521
|
}
|
|
519
522
|
|
|
520
523
|
// src/codex-session-log/token-usage.ts
|
|
524
|
+
function zeroTokenUsage() {
|
|
525
|
+
return {
|
|
526
|
+
inputTokens: 0,
|
|
527
|
+
cachedInputTokens: 0,
|
|
528
|
+
outputTokens: 0,
|
|
529
|
+
reasoningOutputTokens: 0,
|
|
530
|
+
totalTokens: 0,
|
|
531
|
+
practicalBurn: 0
|
|
532
|
+
};
|
|
533
|
+
}
|
|
521
534
|
function readTokenUsage(value, label) {
|
|
522
535
|
const record = expectObject(value, label);
|
|
523
536
|
const inputTokens = readNumber(record, "input_tokens", label);
|
|
@@ -545,7 +558,7 @@ function subtractTokenUsages(currentUsage, previousUsage) {
|
|
|
545
558
|
};
|
|
546
559
|
for (const [key, value] of Object.entries(nextUsage)) {
|
|
547
560
|
if (value < 0) {
|
|
548
|
-
|
|
561
|
+
return null;
|
|
549
562
|
}
|
|
550
563
|
}
|
|
551
564
|
return nextUsage;
|
|
@@ -567,9 +580,11 @@ function extractTokenPoints(records) {
|
|
|
567
580
|
continue;
|
|
568
581
|
}
|
|
569
582
|
const infoRecord = expectObject(info, "event_msg.payload.info");
|
|
583
|
+
const lastUsageValue = infoRecord.last_token_usage;
|
|
570
584
|
tokenPoints.push({
|
|
571
585
|
timestamp: record.timestamp,
|
|
572
|
-
usage: readTokenUsage(infoRecord.total_token_usage, "event_msg.payload.info.total_token_usage")
|
|
586
|
+
usage: readTokenUsage(infoRecord.total_token_usage, "event_msg.payload.info.total_token_usage"),
|
|
587
|
+
lastUsage: lastUsageValue === null || lastUsageValue === undefined ? null : readTokenUsage(lastUsageValue, "event_msg.payload.info.last_token_usage")
|
|
573
588
|
});
|
|
574
589
|
}
|
|
575
590
|
return tokenPoints.sort((leftPoint, rightPoint) => leftPoint.timestamp.getTime() - rightPoint.timestamp.getTime());
|
|
@@ -581,12 +596,21 @@ function buildTokenDeltaPoints(tokenPoints) {
|
|
|
581
596
|
deltaPoints.push({
|
|
582
597
|
timestamp: tokenPoint.timestamp,
|
|
583
598
|
cumulativeUsage: tokenPoint.usage,
|
|
584
|
-
deltaUsage:
|
|
599
|
+
deltaUsage: resolveTokenDeltaUsage(tokenPoint, previousPoint)
|
|
585
600
|
});
|
|
586
601
|
previousPoint = tokenPoint;
|
|
587
602
|
}
|
|
588
603
|
return deltaPoints;
|
|
589
604
|
}
|
|
605
|
+
function resolveTokenDeltaUsage(tokenPoint, previousPoint) {
|
|
606
|
+
if (tokenPoint.lastUsage) {
|
|
607
|
+
return tokenPoint.lastUsage;
|
|
608
|
+
}
|
|
609
|
+
if (!previousPoint) {
|
|
610
|
+
return tokenPoint.usage;
|
|
611
|
+
}
|
|
612
|
+
return subtractTokenUsages(tokenPoint.usage, previousPoint.usage) ?? zeroTokenUsage();
|
|
613
|
+
}
|
|
590
614
|
|
|
591
615
|
// src/codex-session-log/extract-turn-attribution.ts
|
|
592
616
|
function extractTurnAttribution(records) {
|
|
@@ -1037,18 +1061,66 @@ function shortenPath(pathText, maxLength) {
|
|
|
1037
1061
|
return `...${shortenedPath || pathText.slice(-(maxLength - 3))}`;
|
|
1038
1062
|
}
|
|
1039
1063
|
|
|
1064
|
+
// src/reporting/render-best-plaque.ts
|
|
1065
|
+
function buildBestPlaque(ledger) {
|
|
1066
|
+
return {
|
|
1067
|
+
label: "BEST",
|
|
1068
|
+
concurrentAgentsText: `${formatInteger(ledger?.bestConcurrentAgents?.value ?? 0)} concurrent agents`,
|
|
1069
|
+
rawBurnText: `${formatCompactInteger(ledger?.best24hRawBurn?.value ?? 0).toUpperCase()} 24hr raw burn`,
|
|
1070
|
+
agentSumText: `${formatAgentSumHours(ledger?.best24hAgentSumMs?.value ?? 0)} agent sum`
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function buildBestPlaqueRows(bestPlaque, availableWidth) {
|
|
1074
|
+
const wideRows = [
|
|
1075
|
+
bestPlaque.label,
|
|
1076
|
+
bestPlaque.concurrentAgentsText,
|
|
1077
|
+
bestPlaque.rawBurnText,
|
|
1078
|
+
bestPlaque.agentSumText,
|
|
1079
|
+
""
|
|
1080
|
+
];
|
|
1081
|
+
if (rowsFitWidth(wideRows, availableWidth)) {
|
|
1082
|
+
return wideRows;
|
|
1083
|
+
}
|
|
1084
|
+
const compactRows = [
|
|
1085
|
+
bestPlaque.label,
|
|
1086
|
+
bestPlaque.concurrentAgentsText.replace(" agents", ""),
|
|
1087
|
+
bestPlaque.rawBurnText.replace(" 24hr ", " "),
|
|
1088
|
+
bestPlaque.agentSumText,
|
|
1089
|
+
""
|
|
1090
|
+
];
|
|
1091
|
+
if (rowsFitWidth(compactRows, availableWidth)) {
|
|
1092
|
+
return compactRows;
|
|
1093
|
+
}
|
|
1094
|
+
const microRows = [
|
|
1095
|
+
bestPlaque.label,
|
|
1096
|
+
compactRows[1]?.replace("concurrent", "conc") ?? "",
|
|
1097
|
+
compactRows[2]?.replace(" burn", "") ?? "",
|
|
1098
|
+
compactRows[3]?.replace(" agent sum", " sum") ?? "",
|
|
1099
|
+
""
|
|
1100
|
+
];
|
|
1101
|
+
return rowsFitWidth(microRows, availableWidth) ? microRows : null;
|
|
1102
|
+
}
|
|
1103
|
+
function formatAgentSumHours(durationMs) {
|
|
1104
|
+
const hours = durationMs / 3600000;
|
|
1105
|
+
const roundedHours = hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
|
|
1106
|
+
return roundedHours.endsWith(".0") ? roundedHours.slice(0, -2) : roundedHours;
|
|
1107
|
+
}
|
|
1108
|
+
function rowsFitWidth(rows, availableWidth) {
|
|
1109
|
+
return availableWidth > 0 && rows.every((row) => row.length <= availableWidth);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1040
1112
|
// src/reporting/render-theme.ts
|
|
1041
1113
|
var roleStyles = {
|
|
1042
|
-
focus: "1;38;2;
|
|
1043
|
-
active: "1;38;2;
|
|
1044
|
-
agent: "1;38;2;
|
|
1045
|
-
idle: "1;38;2;
|
|
1046
|
-
burn: "1;38;2;
|
|
1047
|
-
raw: "1;38;2;
|
|
1048
|
-
frame: "1;38;2;
|
|
1049
|
-
heading: "1;38;2;
|
|
1050
|
-
muted: "38;2;
|
|
1051
|
-
value: "1;38;2;
|
|
1114
|
+
focus: "1;38;2;190;184;86",
|
|
1115
|
+
active: "1;38;2;157;168;60",
|
|
1116
|
+
agent: "1;38;2;124;138;55",
|
|
1117
|
+
idle: "1;38;2;105;118;50",
|
|
1118
|
+
burn: "1;38;2;190;161;55",
|
|
1119
|
+
raw: "1;38;2;153;132;52",
|
|
1120
|
+
frame: "1;38;2;118;126;50",
|
|
1121
|
+
heading: "1;38;2;201;190;102",
|
|
1122
|
+
muted: "38;2;111;115;78",
|
|
1123
|
+
value: "1;38;2;178;161;68"
|
|
1052
1124
|
};
|
|
1053
1125
|
function createRenderOptions(shareMode) {
|
|
1054
1126
|
return {
|
|
@@ -1086,7 +1158,9 @@ function measureVisibleTextWidth(text) {
|
|
|
1086
1158
|
|
|
1087
1159
|
// src/reporting/render-logo-section.ts
|
|
1088
1160
|
var baseBackgroundStyle = "48;2;12;15;8";
|
|
1089
|
-
var
|
|
1161
|
+
var plaqueInsetColumns = 3;
|
|
1162
|
+
var plaqueTextStyle = "1;38;2;244;235;164";
|
|
1163
|
+
var wordmarkStyle = `${baseBackgroundStyle};1;38;2;210;198;108`;
|
|
1090
1164
|
var wordmarkLines = [
|
|
1091
1165
|
" ▄▄ ▄▄",
|
|
1092
1166
|
"▀▀ ██ ██ ██ ▀▀",
|
|
@@ -1096,59 +1170,76 @@ var wordmarkLines = [
|
|
|
1096
1170
|
];
|
|
1097
1171
|
var patternColors = [
|
|
1098
1172
|
{ red: 20, green: 24, blue: 10 },
|
|
1099
|
-
{ red:
|
|
1100
|
-
{ red:
|
|
1101
|
-
{ red:
|
|
1102
|
-
{ red:
|
|
1103
|
-
{ red:
|
|
1173
|
+
{ red: 42, green: 50, blue: 16 },
|
|
1174
|
+
{ red: 71, green: 81, blue: 24 },
|
|
1175
|
+
{ red: 101, green: 112, blue: 31 },
|
|
1176
|
+
{ red: 136, green: 145, blue: 39 },
|
|
1177
|
+
{ red: 177, green: 169, blue: 58 }
|
|
1104
1178
|
];
|
|
1105
1179
|
var monochromePatternCharacters = ["░", "░", "▒", "▓", "█"];
|
|
1106
|
-
function buildLogoSection(requestedWidth, options) {
|
|
1180
|
+
function buildLogoSection(requestedWidth, options, bestPlaque = null) {
|
|
1107
1181
|
const wordmarkWidth = Math.max(...wordmarkLines.map((line) => line.length));
|
|
1108
1182
|
const sectionWidth = Math.max(requestedWidth, wordmarkWidth);
|
|
1109
1183
|
const patternWidth = Math.max(0, sectionWidth - wordmarkWidth);
|
|
1184
|
+
const plaqueRows = bestPlaque ? buildBestPlaqueRows(bestPlaque, Math.max(0, patternWidth - plaqueInsetColumns)) : null;
|
|
1110
1185
|
return wordmarkLines.map((line, rowIndex) => {
|
|
1111
1186
|
const paddedWordmark = padRight(line, wordmarkWidth);
|
|
1112
|
-
const patternTail = buildPatternTail(patternWidth, rowIndex, options);
|
|
1187
|
+
const patternTail = buildPatternTail(patternWidth, rowIndex, options, plaqueRows?.[rowIndex] ?? "");
|
|
1113
1188
|
return `${paintAnsi(paddedWordmark, wordmarkStyle, options)}${patternTail}`;
|
|
1114
1189
|
});
|
|
1115
1190
|
}
|
|
1116
1191
|
function resolveLogoSectionWidth(minimumWidth, options) {
|
|
1117
1192
|
return Math.max(minimumWidth, options.terminalWidth ?? 0);
|
|
1118
1193
|
}
|
|
1119
|
-
function buildPatternTail(width, rowIndex, options) {
|
|
1194
|
+
function buildPatternTail(width, rowIndex, options, plaqueRowText) {
|
|
1120
1195
|
if (!options.colorEnabled) {
|
|
1121
|
-
return buildMonochromePatternTail(width, rowIndex);
|
|
1196
|
+
return buildMonochromePatternTail(width, rowIndex, plaqueRowText);
|
|
1122
1197
|
}
|
|
1198
|
+
const overlayCharacters = createOverlayCharacters(width, plaqueRowText);
|
|
1199
|
+
const cellStyles = Array.from({ length: width }, (_, columnIndex) => getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex)));
|
|
1123
1200
|
let patternTail = "";
|
|
1124
1201
|
let currentStyle = "";
|
|
1125
|
-
let
|
|
1202
|
+
let currentSegment = "";
|
|
1126
1203
|
for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
|
|
1127
|
-
const
|
|
1204
|
+
const overlayCharacter = overlayCharacters[columnIndex];
|
|
1205
|
+
const style = overlayCharacter === null ? cellStyles[columnIndex] : `${cellStyles[columnIndex]};${plaqueTextStyle}`;
|
|
1128
1206
|
if (style === currentStyle) {
|
|
1129
|
-
|
|
1207
|
+
currentSegment += overlayCharacter ?? " ";
|
|
1130
1208
|
continue;
|
|
1131
1209
|
}
|
|
1132
|
-
if (
|
|
1133
|
-
patternTail += paintAnsi(
|
|
1210
|
+
if (currentSegment.length > 0) {
|
|
1211
|
+
patternTail += paintAnsi(currentSegment, currentStyle, options);
|
|
1134
1212
|
}
|
|
1135
1213
|
currentStyle = style;
|
|
1136
|
-
|
|
1214
|
+
currentSegment = overlayCharacter ?? " ";
|
|
1137
1215
|
}
|
|
1138
|
-
if (
|
|
1139
|
-
patternTail += paintAnsi(
|
|
1216
|
+
if (currentSegment.length > 0) {
|
|
1217
|
+
patternTail += paintAnsi(currentSegment, currentStyle, options);
|
|
1140
1218
|
}
|
|
1141
1219
|
return patternTail;
|
|
1142
1220
|
}
|
|
1143
|
-
function buildMonochromePatternTail(width, rowIndex) {
|
|
1221
|
+
function buildMonochromePatternTail(width, rowIndex, plaqueRowText) {
|
|
1222
|
+
const overlayCharacters = createOverlayCharacters(width, plaqueRowText);
|
|
1144
1223
|
let patternTail = "";
|
|
1145
1224
|
for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
|
|
1225
|
+
const overlayCharacter = overlayCharacters[columnIndex];
|
|
1226
|
+
if (overlayCharacter !== null) {
|
|
1227
|
+
patternTail += overlayCharacter;
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1146
1230
|
const intensity = getPatternIntensity(width, rowIndex, columnIndex);
|
|
1147
1231
|
const characterIndex = Math.min(monochromePatternCharacters.length - 1, Math.floor(intensity * monochromePatternCharacters.length));
|
|
1148
1232
|
patternTail += monochromePatternCharacters[characterIndex];
|
|
1149
1233
|
}
|
|
1150
1234
|
return patternTail;
|
|
1151
1235
|
}
|
|
1236
|
+
function createOverlayCharacters(width, plaqueRowText) {
|
|
1237
|
+
const overlayCharacters = Array.from({ length: width }, () => null);
|
|
1238
|
+
for (let index = 0;index < plaqueRowText.length && plaqueInsetColumns + index < width; index += 1) {
|
|
1239
|
+
overlayCharacters[plaqueInsetColumns + index] = plaqueRowText[index];
|
|
1240
|
+
}
|
|
1241
|
+
return overlayCharacters;
|
|
1242
|
+
}
|
|
1152
1243
|
function getPatternIntensity(width, rowIndex, columnIndex) {
|
|
1153
1244
|
const normalizedColumn = width <= 1 ? 0 : columnIndex / Math.max(1, width - 1);
|
|
1154
1245
|
const envelope = 0.18 + 0.82 * Math.pow(normalizedColumn, 0.82);
|
|
@@ -1331,6 +1422,631 @@ async function runHourlyCommand(command) {
|
|
|
1331
1422
|
}), createRenderOptions(command.shareMode));
|
|
1332
1423
|
}
|
|
1333
1424
|
|
|
1425
|
+
// src/best-metrics/notification-delivery.ts
|
|
1426
|
+
import { execFile } from "node:child_process";
|
|
1427
|
+
import { existsSync } from "node:fs";
|
|
1428
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
1429
|
+
import { promisify } from "node:util";
|
|
1430
|
+
var execFileAsync = promisify(execFile);
|
|
1431
|
+
async function deliverLocalNotifications(notifications, options = {}) {
|
|
1432
|
+
const platform = options.platform ?? process.platform;
|
|
1433
|
+
if (platform !== "darwin" || notifications.length === 0) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const notifier = options.notifier ?? sendMacOsNotification;
|
|
1437
|
+
for (const notification of notifications) {
|
|
1438
|
+
try {
|
|
1439
|
+
await notifier(notification);
|
|
1440
|
+
} catch {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async function sendMacOsNotification(notification) {
|
|
1446
|
+
const notificationIconPath = resolveNotificationIconPath();
|
|
1447
|
+
try {
|
|
1448
|
+
await execFileAsync("terminal-notifier", [
|
|
1449
|
+
"-title",
|
|
1450
|
+
notification.title,
|
|
1451
|
+
"-message",
|
|
1452
|
+
notification.body,
|
|
1453
|
+
...notificationIconPath ? ["-appIcon", pathToFileURL(notificationIconPath).href] : []
|
|
1454
|
+
]);
|
|
1455
|
+
return;
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
if (!isCommandMissingError(error)) {
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
await execFileAsync("osascript", [
|
|
1462
|
+
"-e",
|
|
1463
|
+
`display notification "${escapeAppleScriptText(notification.body)}" with title "${escapeAppleScriptText(notification.title)}"`
|
|
1464
|
+
]);
|
|
1465
|
+
}
|
|
1466
|
+
function escapeAppleScriptText(value) {
|
|
1467
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
|
|
1468
|
+
}
|
|
1469
|
+
function resolveNotificationIconPath() {
|
|
1470
|
+
const candidatePaths = [
|
|
1471
|
+
fileURLToPath(new URL("../../assets/idle-time-notification-icon.png", import.meta.url)),
|
|
1472
|
+
fileURLToPath(new URL("../assets/idle-time-notification-icon.png", import.meta.url))
|
|
1473
|
+
];
|
|
1474
|
+
return candidatePaths.find((candidatePath) => existsSync(candidatePath)) ?? null;
|
|
1475
|
+
}
|
|
1476
|
+
function isCommandMissingError(error) {
|
|
1477
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/best-metrics/near-best-notifications.ts
|
|
1481
|
+
import { mkdir, readFile as readFile2, rename, writeFile } from "node:fs/promises";
|
|
1482
|
+
import { homedir as homedir2 } from "node:os";
|
|
1483
|
+
import { join as join2 } from "node:path";
|
|
1484
|
+
var nearBestNotificationStateFileName = "near-best-notifications-v1.json";
|
|
1485
|
+
var nearBestNotificationVersion = 1;
|
|
1486
|
+
async function notifyNearBestMetrics(currentMetrics, ledger, options = {}) {
|
|
1487
|
+
const now = options.now ?? new Date;
|
|
1488
|
+
const state = await ensureNearBestNotificationState(options);
|
|
1489
|
+
if (!state.nearBestEnabled) {
|
|
1490
|
+
return [];
|
|
1491
|
+
}
|
|
1492
|
+
const metricsToNotify = buildNearBestMetricKeys(currentMetrics, ledger, state, now);
|
|
1493
|
+
if (metricsToNotify.length === 0) {
|
|
1494
|
+
return [];
|
|
1495
|
+
}
|
|
1496
|
+
const nextState = {
|
|
1497
|
+
...state,
|
|
1498
|
+
lastNotifiedAt: {
|
|
1499
|
+
...state.lastNotifiedAt,
|
|
1500
|
+
...Object.fromEntries(metricsToNotify.map((metric) => [metric, now]))
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
await writeNearBestNotificationState(nextState, options);
|
|
1504
|
+
await deliverLocalNotifications(metricsToNotify.map((metric) => buildNearBestNotification(metric, currentMetrics[metric], ledger[metric]?.value ?? 0)), options);
|
|
1505
|
+
return metricsToNotify;
|
|
1506
|
+
}
|
|
1507
|
+
async function ensureNearBestNotificationState(options) {
|
|
1508
|
+
const existingState = await readNearBestNotificationState(options);
|
|
1509
|
+
if (existingState) {
|
|
1510
|
+
return existingState;
|
|
1511
|
+
}
|
|
1512
|
+
const defaultState = createDefaultNearBestNotificationState();
|
|
1513
|
+
await writeNearBestNotificationState(defaultState, options);
|
|
1514
|
+
return defaultState;
|
|
1515
|
+
}
|
|
1516
|
+
async function readNearBestNotificationState(options) {
|
|
1517
|
+
try {
|
|
1518
|
+
const rawStateText = await readFile2(resolveNearBestNotificationStatePath(options), "utf8");
|
|
1519
|
+
return parseNearBestNotificationState(JSON.parse(rawStateText));
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
throw error;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function parseNearBestNotificationState(value) {
|
|
1528
|
+
const stateRecord = expectObject(value, "nearBestNotificationState");
|
|
1529
|
+
const version = readNumber(stateRecord, "version", "nearBestNotificationState");
|
|
1530
|
+
if (version !== nearBestNotificationVersion) {
|
|
1531
|
+
throw new Error(`nearBestNotificationState.version must be ${nearBestNotificationVersion}.`);
|
|
1532
|
+
}
|
|
1533
|
+
const lastNotifiedAtRecord = expectObject(stateRecord.lastNotifiedAt, "nearBestNotificationState.lastNotifiedAt");
|
|
1534
|
+
return {
|
|
1535
|
+
version,
|
|
1536
|
+
nearBestEnabled: Boolean(stateRecord.nearBestEnabled),
|
|
1537
|
+
thresholdRatio: readNumber(stateRecord, "thresholdRatio", "nearBestNotificationState"),
|
|
1538
|
+
cooldownMs: readNumber(stateRecord, "cooldownMs", "nearBestNotificationState"),
|
|
1539
|
+
lastNotifiedAt: {
|
|
1540
|
+
bestConcurrentAgents: readOptionalIsoTimestamp(lastNotifiedAtRecord.bestConcurrentAgents, "nearBestNotificationState.lastNotifiedAt.bestConcurrentAgents"),
|
|
1541
|
+
best24hRawBurn: readOptionalIsoTimestamp(lastNotifiedAtRecord.best24hRawBurn, "nearBestNotificationState.lastNotifiedAt.best24hRawBurn"),
|
|
1542
|
+
best24hAgentSumMs: readOptionalIsoTimestamp(lastNotifiedAtRecord.best24hAgentSumMs, "nearBestNotificationState.lastNotifiedAt.best24hAgentSumMs")
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
async function writeNearBestNotificationState(state, options) {
|
|
1547
|
+
const statePath = resolveNearBestNotificationStatePath(options);
|
|
1548
|
+
const stateDirectory = options.stateDirectory ?? join2(homedir2(), ".idletime");
|
|
1549
|
+
await mkdir(stateDirectory, { recursive: true });
|
|
1550
|
+
const temporaryPath = join2(stateDirectory, `.near-best-notifications.${process.pid}.${Date.now()}.tmp`);
|
|
1551
|
+
await writeFile(temporaryPath, `${JSON.stringify({
|
|
1552
|
+
version: state.version,
|
|
1553
|
+
nearBestEnabled: state.nearBestEnabled,
|
|
1554
|
+
thresholdRatio: state.thresholdRatio,
|
|
1555
|
+
cooldownMs: state.cooldownMs,
|
|
1556
|
+
lastNotifiedAt: {
|
|
1557
|
+
bestConcurrentAgents: state.lastNotifiedAt.bestConcurrentAgents?.toISOString() ?? null,
|
|
1558
|
+
best24hRawBurn: state.lastNotifiedAt.best24hRawBurn?.toISOString() ?? null,
|
|
1559
|
+
best24hAgentSumMs: state.lastNotifiedAt.best24hAgentSumMs?.toISOString() ?? null
|
|
1560
|
+
}
|
|
1561
|
+
}, null, 2)}
|
|
1562
|
+
`, "utf8");
|
|
1563
|
+
await rename(temporaryPath, statePath);
|
|
1564
|
+
}
|
|
1565
|
+
function buildNearBestMetricKeys(currentMetrics, ledger, state, now) {
|
|
1566
|
+
return [
|
|
1567
|
+
"bestConcurrentAgents",
|
|
1568
|
+
"best24hRawBurn",
|
|
1569
|
+
"best24hAgentSumMs"
|
|
1570
|
+
].filter((metric) => {
|
|
1571
|
+
const bestValue = ledger[metric]?.value ?? 0;
|
|
1572
|
+
if (bestValue <= 0) {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
const currentValue = currentMetrics[metric];
|
|
1576
|
+
if (currentValue <= 0 || currentValue >= bestValue) {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
if (currentValue / bestValue < state.thresholdRatio) {
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
const lastNotifiedAt = state.lastNotifiedAt[metric];
|
|
1583
|
+
return lastNotifiedAt === null || now.getTime() - lastNotifiedAt.getTime() >= state.cooldownMs;
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
function buildNearBestNotification(metric, currentValue, bestValue) {
|
|
1587
|
+
return {
|
|
1588
|
+
title: metric === "bestConcurrentAgents" ? "Close to best concurrent agents" : metric === "best24hRawBurn" ? "Close to best 24hr raw burn" : "Close to best agent sum",
|
|
1589
|
+
body: metric === "bestConcurrentAgents" ? `${formatInteger2(currentValue)} of ${formatInteger2(bestValue)} concurrent agents` : metric === "best24hRawBurn" ? `${formatCompactInteger2(currentValue)} of ${formatCompactInteger2(bestValue)} 24hr raw burn` : `${formatAgentSumHours2(currentValue)} of ${formatAgentSumHours2(bestValue)} agent sum`
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function createDefaultNearBestNotificationState() {
|
|
1593
|
+
return {
|
|
1594
|
+
version: nearBestNotificationVersion,
|
|
1595
|
+
nearBestEnabled: false,
|
|
1596
|
+
thresholdRatio: 0.97,
|
|
1597
|
+
cooldownMs: 24 * 60 * 60 * 1000,
|
|
1598
|
+
lastNotifiedAt: {
|
|
1599
|
+
bestConcurrentAgents: null,
|
|
1600
|
+
best24hRawBurn: null,
|
|
1601
|
+
best24hAgentSumMs: null
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
function resolveNearBestNotificationStatePath(options) {
|
|
1606
|
+
return join2(options.stateDirectory ?? join2(homedir2(), ".idletime"), nearBestNotificationStateFileName);
|
|
1607
|
+
}
|
|
1608
|
+
function readOptionalIsoTimestamp(value, label) {
|
|
1609
|
+
if (value === null || value === undefined) {
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
return readIsoTimestamp(value, label);
|
|
1613
|
+
}
|
|
1614
|
+
function formatAgentSumHours2(durationMs) {
|
|
1615
|
+
const hours = durationMs / 3600000;
|
|
1616
|
+
return hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
|
|
1617
|
+
}
|
|
1618
|
+
function formatCompactInteger2(value) {
|
|
1619
|
+
return new Intl.NumberFormat("en-US", {
|
|
1620
|
+
notation: "compact",
|
|
1621
|
+
maximumFractionDigits: 1
|
|
1622
|
+
}).format(Math.round(value)).toUpperCase();
|
|
1623
|
+
}
|
|
1624
|
+
function formatInteger2(value) {
|
|
1625
|
+
return new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/best-metrics/notify-best-events.ts
|
|
1629
|
+
async function notifyBestEvents(bestEvents, options = {}) {
|
|
1630
|
+
await deliverLocalNotifications(bestEvents.map((bestEvent) => buildBestEventNotification(bestEvent)), options);
|
|
1631
|
+
}
|
|
1632
|
+
function buildBestEventNotification(bestEvent) {
|
|
1633
|
+
return {
|
|
1634
|
+
title: resolveNotificationTitle(bestEvent.metric),
|
|
1635
|
+
body: resolveNotificationBody(bestEvent)
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function resolveNotificationTitle(bestMetricKey) {
|
|
1639
|
+
return bestMetricKey === "bestConcurrentAgents" ? "New best concurrent agents" : bestMetricKey === "best24hRawBurn" ? "New best 24hr raw burn" : "New best agent sum";
|
|
1640
|
+
}
|
|
1641
|
+
function resolveNotificationBody(bestEvent) {
|
|
1642
|
+
return bestEvent.metric === "bestConcurrentAgents" ? `${formatInteger3(bestEvent.value)} concurrent agents` : bestEvent.metric === "best24hRawBurn" ? `${formatCompactInteger3(bestEvent.value)} 24hr raw burn` : `${formatAgentSumHours3(bestEvent.value)} agent sum`;
|
|
1643
|
+
}
|
|
1644
|
+
function formatAgentSumHours3(durationMs) {
|
|
1645
|
+
const hours = durationMs / 3600000;
|
|
1646
|
+
return hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
|
|
1647
|
+
}
|
|
1648
|
+
function formatCompactInteger3(value) {
|
|
1649
|
+
return new Intl.NumberFormat("en-US", {
|
|
1650
|
+
notation: "compact",
|
|
1651
|
+
maximumFractionDigits: 1
|
|
1652
|
+
}).format(Math.round(value)).toUpperCase();
|
|
1653
|
+
}
|
|
1654
|
+
function formatInteger3(value) {
|
|
1655
|
+
return new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// src/best-metrics/types.ts
|
|
1659
|
+
var bestMetricsLedgerVersion = 1;
|
|
1660
|
+
var rollingWindowDurationMs = 24 * 60 * 60 * 1000;
|
|
1661
|
+
var defaultBestMetricsIdleCutoffMs = 15 * 60 * 1000;
|
|
1662
|
+
|
|
1663
|
+
// src/best-metrics/build-current-best-metrics.ts
|
|
1664
|
+
function buildCurrentBestMetricValues(sessions, options = {}) {
|
|
1665
|
+
const idleCutoffMs = options.idleCutoffMs ?? defaultBestMetricsIdleCutoffMs;
|
|
1666
|
+
const now = options.now ?? new Date;
|
|
1667
|
+
const activityMetrics = buildActivityMetrics(sessions, idleCutoffMs);
|
|
1668
|
+
const currentWindow = {
|
|
1669
|
+
start: new Date(now.getTime() - rollingWindowDurationMs),
|
|
1670
|
+
end: now
|
|
1671
|
+
};
|
|
1672
|
+
return {
|
|
1673
|
+
bestConcurrentAgents: countLiveSubagents(activityMetrics.perSubagentBlocks, now),
|
|
1674
|
+
best24hRawBurn: sessions.reduce((rawBurnTotal, session) => rawBurnTotal + buildTokenDeltaPoints(session.tokenPoints).filter((tokenDeltaPoint) => tokenDeltaPoint.timestamp.getTime() >= currentWindow.start.getTime() && tokenDeltaPoint.timestamp.getTime() <= currentWindow.end.getTime()).reduce((sessionTotal, tokenDeltaPoint) => sessionTotal + tokenDeltaPoint.deltaUsage.totalTokens, 0), 0),
|
|
1675
|
+
best24hAgentSumMs: measureOverlapMs(activityMetrics.perSubagentBlocks.flatMap((sessionBlocks) => sessionBlocks), currentWindow)
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function countLiveSubagents(intervalGroups, now) {
|
|
1679
|
+
return intervalGroups.reduce((liveCount, intervalGroup) => liveCount + Number(intervalGroup.some((interval) => interval.start.getTime() <= now.getTime() && interval.end.getTime() > now.getTime())), 0);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/best-metrics/append-best-events.ts
|
|
1683
|
+
import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
|
|
1684
|
+
import { homedir as homedir3 } from "node:os";
|
|
1685
|
+
import { join as join3 } from "node:path";
|
|
1686
|
+
var bestEventsFileName = "best-events.ndjson";
|
|
1687
|
+
async function appendBestEvents(bestEvents, options = {}) {
|
|
1688
|
+
if (bestEvents.length === 0) {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
const stateDirectory = resolveBestStateDirectory(options);
|
|
1692
|
+
await mkdir2(stateDirectory, { recursive: true });
|
|
1693
|
+
await appendFile(join3(stateDirectory, bestEventsFileName), `${bestEvents.map(serializeBestEvent).join(`
|
|
1694
|
+
`)}
|
|
1695
|
+
`, "utf8");
|
|
1696
|
+
}
|
|
1697
|
+
function serializeBestEvent(bestEvent) {
|
|
1698
|
+
return JSON.stringify({
|
|
1699
|
+
metric: bestEvent.metric,
|
|
1700
|
+
previousValue: bestEvent.previousValue,
|
|
1701
|
+
value: bestEvent.value,
|
|
1702
|
+
observedAt: bestEvent.observedAt.toISOString(),
|
|
1703
|
+
windowStart: bestEvent.windowStart.toISOString(),
|
|
1704
|
+
windowEnd: bestEvent.windowEnd.toISOString(),
|
|
1705
|
+
version: bestEvent.version
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
function resolveBestStateDirectory(options) {
|
|
1709
|
+
return options.stateDirectory ?? join3(homedir3(), ".idletime");
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/best-metrics/build-rolling-24h-windows.ts
|
|
1713
|
+
function findBestRollingWindowTotal(weightedPoints) {
|
|
1714
|
+
const sortedPoints = weightedPoints.filter((point) => point.value > 0).slice().sort((leftPoint, rightPoint) => leftPoint.timestamp.getTime() - rightPoint.timestamp.getTime());
|
|
1715
|
+
if (sortedPoints.length === 0) {
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
let bestValue = 0;
|
|
1719
|
+
let bestTimestampMs = 0;
|
|
1720
|
+
let currentTotal = 0;
|
|
1721
|
+
let leftIndex = 0;
|
|
1722
|
+
for (let rightIndex = 0;rightIndex < sortedPoints.length; rightIndex += 1) {
|
|
1723
|
+
const rightPoint = sortedPoints[rightIndex];
|
|
1724
|
+
currentTotal += rightPoint.value;
|
|
1725
|
+
while (rightPoint.timestamp.getTime() - sortedPoints[leftIndex].timestamp.getTime() > rollingWindowDurationMs) {
|
|
1726
|
+
currentTotal -= sortedPoints[leftIndex].value;
|
|
1727
|
+
leftIndex += 1;
|
|
1728
|
+
}
|
|
1729
|
+
if (currentTotal > bestValue) {
|
|
1730
|
+
bestValue = currentTotal;
|
|
1731
|
+
bestTimestampMs = rightPoint.timestamp.getTime();
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
if (bestValue === 0) {
|
|
1735
|
+
return null;
|
|
1736
|
+
}
|
|
1737
|
+
return createRollingRecord(bestValue, bestTimestampMs);
|
|
1738
|
+
}
|
|
1739
|
+
function findBestRollingWindowOverlap(intervals) {
|
|
1740
|
+
const slopeChanges = intervals.flatMap(buildSlopeChanges);
|
|
1741
|
+
if (slopeChanges.length === 0) {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
slopeChanges.sort((leftChange, rightChange) => leftChange.timestampMs - rightChange.timestampMs);
|
|
1745
|
+
let bestTimestampMs = 0;
|
|
1746
|
+
let bestValue = 0;
|
|
1747
|
+
let currentSlope = 0;
|
|
1748
|
+
let currentValue = 0;
|
|
1749
|
+
let previousTimestampMs = slopeChanges[0].timestampMs;
|
|
1750
|
+
let index = 0;
|
|
1751
|
+
while (index < slopeChanges.length) {
|
|
1752
|
+
const timestampMs = slopeChanges[index].timestampMs;
|
|
1753
|
+
currentValue += currentSlope * (timestampMs - previousTimestampMs);
|
|
1754
|
+
if (currentValue > bestValue) {
|
|
1755
|
+
bestValue = currentValue;
|
|
1756
|
+
bestTimestampMs = timestampMs;
|
|
1757
|
+
}
|
|
1758
|
+
while (index < slopeChanges.length && slopeChanges[index].timestampMs === timestampMs) {
|
|
1759
|
+
currentSlope += slopeChanges[index].deltaSlope;
|
|
1760
|
+
index += 1;
|
|
1761
|
+
}
|
|
1762
|
+
previousTimestampMs = timestampMs;
|
|
1763
|
+
}
|
|
1764
|
+
if (bestValue === 0) {
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
return createRollingRecord(bestValue, bestTimestampMs);
|
|
1768
|
+
}
|
|
1769
|
+
function buildSlopeChanges(interval) {
|
|
1770
|
+
const startMs = interval.start.getTime();
|
|
1771
|
+
const endMs = interval.end.getTime();
|
|
1772
|
+
if (endMs <= startMs) {
|
|
1773
|
+
return [];
|
|
1774
|
+
}
|
|
1775
|
+
return [
|
|
1776
|
+
{ timestampMs: startMs, deltaSlope: 1 },
|
|
1777
|
+
{ timestampMs: endMs, deltaSlope: -1 },
|
|
1778
|
+
{ timestampMs: startMs + rollingWindowDurationMs, deltaSlope: -1 },
|
|
1779
|
+
{ timestampMs: endMs + rollingWindowDurationMs, deltaSlope: 1 }
|
|
1780
|
+
];
|
|
1781
|
+
}
|
|
1782
|
+
function createRollingRecord(value, observedAtMs) {
|
|
1783
|
+
return {
|
|
1784
|
+
value,
|
|
1785
|
+
observedAt: new Date(observedAtMs),
|
|
1786
|
+
windowStart: new Date(observedAtMs - rollingWindowDurationMs),
|
|
1787
|
+
windowEnd: new Date(observedAtMs)
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// src/best-metrics/build-best-metrics.ts
|
|
1792
|
+
function buildBestMetricCandidates(sessions, options = {}) {
|
|
1793
|
+
const idleCutoffMs = options.idleCutoffMs ?? defaultBestMetricsIdleCutoffMs;
|
|
1794
|
+
const activityMetrics = buildActivityMetrics(sessions, idleCutoffMs);
|
|
1795
|
+
return {
|
|
1796
|
+
bestConcurrentAgents: findBestConcurrentAgents(activityMetrics.perSubagentBlocks),
|
|
1797
|
+
best24hRawBurn: findBestRollingWindowTotal(sessions.flatMap((session) => buildTokenDeltaPoints(session.tokenPoints).map((tokenDeltaPoint) => ({
|
|
1798
|
+
timestamp: tokenDeltaPoint.timestamp,
|
|
1799
|
+
value: tokenDeltaPoint.deltaUsage.totalTokens
|
|
1800
|
+
})))),
|
|
1801
|
+
best24hAgentSumMs: findBestRollingWindowOverlap(activityMetrics.perSubagentBlocks.flatMap((sessionBlocks) => sessionBlocks))
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function findBestConcurrentAgents(intervalGroups) {
|
|
1805
|
+
const concurrencyEdges = intervalGroups.flatMap((intervalGroup) => intervalGroup.flatMap((interval) => [
|
|
1806
|
+
{ timestampMs: interval.start.getTime(), delta: 1 },
|
|
1807
|
+
{ timestampMs: interval.end.getTime(), delta: -1 }
|
|
1808
|
+
]));
|
|
1809
|
+
if (concurrencyEdges.length === 0) {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
concurrencyEdges.sort((leftEdge, rightEdge) => leftEdge.timestampMs - rightEdge.timestampMs);
|
|
1813
|
+
let activeCount = 0;
|
|
1814
|
+
let bestRecord = null;
|
|
1815
|
+
let index = 0;
|
|
1816
|
+
while (index < concurrencyEdges.length) {
|
|
1817
|
+
const timestampMs = concurrencyEdges[index].timestampMs;
|
|
1818
|
+
while (index < concurrencyEdges.length && concurrencyEdges[index].timestampMs === timestampMs) {
|
|
1819
|
+
activeCount += concurrencyEdges[index].delta;
|
|
1820
|
+
index += 1;
|
|
1821
|
+
}
|
|
1822
|
+
const nextTimestampMs = concurrencyEdges[index]?.timestampMs ?? timestampMs;
|
|
1823
|
+
if (nextTimestampMs <= timestampMs || activeCount <= 0) {
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (!bestRecord || activeCount > bestRecord.value) {
|
|
1827
|
+
bestRecord = {
|
|
1828
|
+
value: activeCount,
|
|
1829
|
+
observedAt: new Date(timestampMs),
|
|
1830
|
+
windowStart: new Date(timestampMs),
|
|
1831
|
+
windowEnd: new Date(nextTimestampMs)
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return bestRecord;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/best-metrics/read-all-codex-sessions.ts
|
|
1839
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
1840
|
+
import { homedir as homedir4 } from "node:os";
|
|
1841
|
+
import { join as join4 } from "node:path";
|
|
1842
|
+
var defaultSessionRootDirectory2 = join4(homedir4(), ".codex", "sessions");
|
|
1843
|
+
async function readAllCodexSessions(options = {}) {
|
|
1844
|
+
const sessionRootDirectory = options.sessionRootDirectory ?? defaultSessionRootDirectory2;
|
|
1845
|
+
const sessionFiles = await listAllSessionFiles(sessionRootDirectory);
|
|
1846
|
+
const parsedSessionResults = await Promise.allSettled(sessionFiles.map((sessionFilePath) => parseCodexSession(sessionFilePath)));
|
|
1847
|
+
const parsedSessions = parsedSessionResults.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
1848
|
+
return parsedSessions.sort((leftSession, rightSession) => leftSession.firstTimestamp.getTime() - rightSession.firstTimestamp.getTime());
|
|
1849
|
+
}
|
|
1850
|
+
async function listAllSessionFiles(rootDirectory) {
|
|
1851
|
+
const pendingDirectories = [rootDirectory];
|
|
1852
|
+
const sessionFiles = [];
|
|
1853
|
+
while (pendingDirectories.length > 0) {
|
|
1854
|
+
const currentDirectory = pendingDirectories.pop();
|
|
1855
|
+
const directoryEntries = await readDirectoryEntries2(currentDirectory);
|
|
1856
|
+
for (const directoryEntry of directoryEntries) {
|
|
1857
|
+
const entryPath = join4(currentDirectory, directoryEntry.name);
|
|
1858
|
+
if (directoryEntry.isDirectory()) {
|
|
1859
|
+
pendingDirectories.push(entryPath);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
if (directoryEntry.isFile() && directoryEntry.name.endsWith(".jsonl")) {
|
|
1863
|
+
sessionFiles.push(entryPath);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return sessionFiles.sort();
|
|
1868
|
+
}
|
|
1869
|
+
async function readDirectoryEntries2(directoryPath) {
|
|
1870
|
+
try {
|
|
1871
|
+
return await readdir2(directoryPath, { withFileTypes: true });
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
if (isMissingDirectoryError2(error)) {
|
|
1874
|
+
return [];
|
|
1875
|
+
}
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
function isMissingDirectoryError2(error) {
|
|
1880
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/best-metrics/read-best-ledger.ts
|
|
1884
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1885
|
+
import { homedir as homedir5 } from "node:os";
|
|
1886
|
+
import { join as join5 } from "node:path";
|
|
1887
|
+
var bestLedgerFileName = "bests-v1.json";
|
|
1888
|
+
async function readBestLedger(options = {}) {
|
|
1889
|
+
try {
|
|
1890
|
+
const rawLedgerText = await readFile3(resolveBestLedgerPath(options), "utf8");
|
|
1891
|
+
return parseBestLedger(JSON.parse(rawLedgerText));
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
if (isMissingFileError(error)) {
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
throw error;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
function parseBestLedger(value) {
|
|
1900
|
+
const ledgerRecord = expectObject(value, "bestMetricsLedger");
|
|
1901
|
+
const version = readNumber(ledgerRecord, "version", "bestMetricsLedger");
|
|
1902
|
+
if (version !== bestMetricsLedgerVersion) {
|
|
1903
|
+
throw new Error(`bestMetricsLedger.version must be ${bestMetricsLedgerVersion}.`);
|
|
1904
|
+
}
|
|
1905
|
+
return {
|
|
1906
|
+
version: bestMetricsLedgerVersion,
|
|
1907
|
+
initializedAt: readIsoTimestamp(ledgerRecord.initializedAt, "bestMetricsLedger.initializedAt"),
|
|
1908
|
+
lastScannedAt: readIsoTimestamp(ledgerRecord.lastScannedAt, "bestMetricsLedger.lastScannedAt"),
|
|
1909
|
+
bestConcurrentAgents: parseBestMetricRecord(ledgerRecord.bestConcurrentAgents, "bestMetricsLedger.bestConcurrentAgents"),
|
|
1910
|
+
best24hRawBurn: parseBestMetricRecord(ledgerRecord.best24hRawBurn, "bestMetricsLedger.best24hRawBurn"),
|
|
1911
|
+
best24hAgentSumMs: parseBestMetricRecord(ledgerRecord.best24hAgentSumMs, "bestMetricsLedger.best24hAgentSumMs")
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function resolveBestLedgerPath(options = {}) {
|
|
1915
|
+
return join5(resolveBestStateDirectory2(options), bestLedgerFileName);
|
|
1916
|
+
}
|
|
1917
|
+
function serializeBestLedger(ledger) {
|
|
1918
|
+
const serializedLedger = {
|
|
1919
|
+
version: ledger.version,
|
|
1920
|
+
initializedAt: ledger.initializedAt.toISOString(),
|
|
1921
|
+
lastScannedAt: ledger.lastScannedAt.toISOString(),
|
|
1922
|
+
bestConcurrentAgents: serializeBestMetricRecord(ledger.bestConcurrentAgents),
|
|
1923
|
+
best24hRawBurn: serializeBestMetricRecord(ledger.best24hRawBurn),
|
|
1924
|
+
best24hAgentSumMs: serializeBestMetricRecord(ledger.best24hAgentSumMs)
|
|
1925
|
+
};
|
|
1926
|
+
return `${JSON.stringify(serializedLedger, null, 2)}
|
|
1927
|
+
`;
|
|
1928
|
+
}
|
|
1929
|
+
function parseBestMetricRecord(value, label) {
|
|
1930
|
+
if (value === null || value === undefined) {
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
const record = expectObject(value, label);
|
|
1934
|
+
return {
|
|
1935
|
+
value: readNumber(record, "value", label),
|
|
1936
|
+
observedAt: readIsoTimestamp(record.observedAt, `${label}.observedAt`),
|
|
1937
|
+
windowStart: readIsoTimestamp(record.windowStart, `${label}.windowStart`),
|
|
1938
|
+
windowEnd: readIsoTimestamp(record.windowEnd, `${label}.windowEnd`)
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
function serializeBestMetricRecord(record) {
|
|
1942
|
+
if (!record) {
|
|
1943
|
+
return null;
|
|
1944
|
+
}
|
|
1945
|
+
return {
|
|
1946
|
+
value: record.value,
|
|
1947
|
+
observedAt: record.observedAt.toISOString(),
|
|
1948
|
+
windowStart: record.windowStart.toISOString(),
|
|
1949
|
+
windowEnd: record.windowEnd.toISOString()
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
function resolveBestStateDirectory2(options) {
|
|
1953
|
+
return options.stateDirectory ?? join5(homedir5(), ".idletime");
|
|
1954
|
+
}
|
|
1955
|
+
function isMissingFileError(error) {
|
|
1956
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/best-metrics/write-best-ledger.ts
|
|
1960
|
+
import { mkdir as mkdir3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1961
|
+
import { join as join6 } from "node:path";
|
|
1962
|
+
async function writeBestLedger(ledger, options = {}) {
|
|
1963
|
+
const ledgerPath = resolveBestLedgerPath(options);
|
|
1964
|
+
const stateDirectory = options.stateDirectory ?? ledgerPath.slice(0, ledgerPath.lastIndexOf("/"));
|
|
1965
|
+
await mkdir3(stateDirectory, { recursive: true });
|
|
1966
|
+
const temporaryPath = join6(stateDirectory, `.bests-v1.${process.pid}.${Date.now()}.tmp`);
|
|
1967
|
+
await writeFile2(temporaryPath, serializeBestLedger(ledger), "utf8");
|
|
1968
|
+
await rename2(temporaryPath, ledgerPath);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/best-metrics/refresh-best-metrics.ts
|
|
1972
|
+
async function refreshBestMetrics(options = {}) {
|
|
1973
|
+
const refreshedAt = options.now ?? new Date;
|
|
1974
|
+
const existingLedger = await readBestLedger(options);
|
|
1975
|
+
const sessions = await readAllCodexSessions({
|
|
1976
|
+
sessionRootDirectory: options.sessionRootDirectory
|
|
1977
|
+
});
|
|
1978
|
+
const bestMetricCandidates = buildBestMetricCandidates(sessions);
|
|
1979
|
+
const currentMetrics = buildCurrentBestMetricValues(sessions, {
|
|
1980
|
+
now: refreshedAt
|
|
1981
|
+
});
|
|
1982
|
+
if (!existingLedger) {
|
|
1983
|
+
const bootstrappedLedger = {
|
|
1984
|
+
version: bestMetricsLedgerVersion,
|
|
1985
|
+
initializedAt: refreshedAt,
|
|
1986
|
+
lastScannedAt: refreshedAt,
|
|
1987
|
+
...bestMetricCandidates
|
|
1988
|
+
};
|
|
1989
|
+
await writeBestLedger(bootstrappedLedger, options);
|
|
1990
|
+
return {
|
|
1991
|
+
currentMetrics,
|
|
1992
|
+
ledger: bootstrappedLedger,
|
|
1993
|
+
newBestEvents: [],
|
|
1994
|
+
refreshMode: "bootstrap"
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
const newBestEvents = buildNewBestEvents(existingLedger, bestMetricCandidates);
|
|
1998
|
+
const refreshedLedger = {
|
|
1999
|
+
...existingLedger,
|
|
2000
|
+
lastScannedAt: refreshedAt,
|
|
2001
|
+
...mergeBestMetricCandidates(existingLedger, bestMetricCandidates)
|
|
2002
|
+
};
|
|
2003
|
+
await writeBestLedger(refreshedLedger, options);
|
|
2004
|
+
await appendBestEvents(newBestEvents, options);
|
|
2005
|
+
return {
|
|
2006
|
+
currentMetrics,
|
|
2007
|
+
ledger: refreshedLedger,
|
|
2008
|
+
newBestEvents,
|
|
2009
|
+
refreshMode: "refresh"
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
function mergeBestMetricCandidates(currentLedger, candidateLedger) {
|
|
2013
|
+
return {
|
|
2014
|
+
bestConcurrentAgents: pickBetterRecord(currentLedger.bestConcurrentAgents, candidateLedger.bestConcurrentAgents),
|
|
2015
|
+
best24hRawBurn: pickBetterRecord(currentLedger.best24hRawBurn, candidateLedger.best24hRawBurn),
|
|
2016
|
+
best24hAgentSumMs: pickBetterRecord(currentLedger.best24hAgentSumMs, candidateLedger.best24hAgentSumMs)
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function pickBetterRecord(currentRecord, candidateRecord) {
|
|
2020
|
+
if (!candidateRecord) {
|
|
2021
|
+
return currentRecord;
|
|
2022
|
+
}
|
|
2023
|
+
if (!currentRecord || candidateRecord.value > currentRecord.value) {
|
|
2024
|
+
return candidateRecord;
|
|
2025
|
+
}
|
|
2026
|
+
return currentRecord;
|
|
2027
|
+
}
|
|
2028
|
+
function buildNewBestEvents(currentLedger, candidateLedger) {
|
|
2029
|
+
return [
|
|
2030
|
+
buildNewBestEvent("bestConcurrentAgents", currentLedger.bestConcurrentAgents, candidateLedger.bestConcurrentAgents),
|
|
2031
|
+
buildNewBestEvent("best24hRawBurn", currentLedger.best24hRawBurn, candidateLedger.best24hRawBurn),
|
|
2032
|
+
buildNewBestEvent("best24hAgentSumMs", currentLedger.best24hAgentSumMs, candidateLedger.best24hAgentSumMs)
|
|
2033
|
+
].flatMap((bestEvent) => bestEvent ? [bestEvent] : []);
|
|
2034
|
+
}
|
|
2035
|
+
function buildNewBestEvent(metric, currentRecord, candidateRecord) {
|
|
2036
|
+
if (!candidateRecord || currentRecord !== null && candidateRecord.value <= currentRecord.value) {
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
return {
|
|
2040
|
+
metric,
|
|
2041
|
+
previousValue: currentRecord?.value ?? null,
|
|
2042
|
+
value: candidateRecord.value,
|
|
2043
|
+
observedAt: candidateRecord.observedAt,
|
|
2044
|
+
windowStart: candidateRecord.windowStart,
|
|
2045
|
+
windowEnd: candidateRecord.windowEnd,
|
|
2046
|
+
version: bestMetricsLedgerVersion
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1334
2050
|
// src/reporting/build-summary-report.ts
|
|
1335
2051
|
function buildSummaryReport(sessions, query) {
|
|
1336
2052
|
const filteredSessions = filterSessions(sessions, query.filters);
|
|
@@ -1428,23 +2144,19 @@ function clipActivityMetricsToWindow(metrics, windowInterval) {
|
|
|
1428
2144
|
|
|
1429
2145
|
// src/reporting/render-summary-report.ts
|
|
1430
2146
|
var summaryBarWidth = 18;
|
|
1431
|
-
function renderSummaryReport(report, options, hourlyReport) {
|
|
1432
|
-
return options.shareMode ? renderShareSummaryReport(report, options, hourlyReport) : renderFullSummaryReport(report, options, hourlyReport);
|
|
2147
|
+
function renderSummaryReport(report, options, hourlyReport, bestPlaque = null) {
|
|
2148
|
+
return options.shareMode ? renderShareSummaryReport(report, options, hourlyReport, bestPlaque) : renderFullSummaryReport(report, options, hourlyReport, bestPlaque);
|
|
1433
2149
|
}
|
|
1434
|
-
function renderFullSummaryReport(report, options, hourlyReport) {
|
|
2150
|
+
function renderFullSummaryReport(report, options, hourlyReport, bestPlaque = null) {
|
|
1435
2151
|
const lines = [];
|
|
1436
2152
|
const requestedMetrics = report.metrics;
|
|
1437
2153
|
const actualComparisonMetrics = report.comparisonMetrics;
|
|
1438
2154
|
const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
|
|
1439
|
-
const headerLines =
|
|
1440
|
-
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
1441
|
-
`${report.sessionCounts.total} sessions · ${formatDurationHours(requestedMetrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
|
|
1442
|
-
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
1443
|
-
];
|
|
2155
|
+
const headerLines = buildSummaryHeaderLines(report, hourlyReport);
|
|
1444
2156
|
const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
|
|
1445
2157
|
const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
|
|
1446
2158
|
const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
|
|
1447
|
-
lines.push(...buildLogoSection(logoSectionWidth, options));
|
|
2159
|
+
lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
|
|
1448
2160
|
lines.push("");
|
|
1449
2161
|
lines.push(...panelLines);
|
|
1450
2162
|
if (hourlyReport) {
|
|
@@ -1495,17 +2207,13 @@ function renderFullSummaryReport(report, options, hourlyReport) {
|
|
|
1495
2207
|
return lines.join(`
|
|
1496
2208
|
`);
|
|
1497
2209
|
}
|
|
1498
|
-
function renderShareSummaryReport(report, options, hourlyReport) {
|
|
2210
|
+
function renderShareSummaryReport(report, options, hourlyReport, bestPlaque = null) {
|
|
1499
2211
|
const lines = [];
|
|
1500
|
-
const headerLines =
|
|
1501
|
-
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
1502
|
-
`${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
|
|
1503
|
-
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
1504
|
-
];
|
|
2212
|
+
const headerLines = buildSummaryHeaderLines(report, hourlyReport);
|
|
1505
2213
|
const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
|
|
1506
2214
|
const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
|
|
1507
2215
|
const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
|
|
1508
|
-
lines.push(...buildLogoSection(logoSectionWidth, options));
|
|
2216
|
+
lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
|
|
1509
2217
|
lines.push("");
|
|
1510
2218
|
lines.push(...panelLines);
|
|
1511
2219
|
if (hourlyReport) {
|
|
@@ -1539,6 +2247,93 @@ function formatAppliedFilters(report) {
|
|
|
1539
2247
|
}
|
|
1540
2248
|
return appliedFilters;
|
|
1541
2249
|
}
|
|
2250
|
+
function buildSummaryHeaderLines(report, hourlyReport) {
|
|
2251
|
+
if (!hourlyReport) {
|
|
2252
|
+
return [
|
|
2253
|
+
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
2254
|
+
`${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
|
|
2255
|
+
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
2256
|
+
];
|
|
2257
|
+
}
|
|
2258
|
+
return [
|
|
2259
|
+
buildPostureLine(report, hourlyReport),
|
|
2260
|
+
buildBiggestStoryLine(report, hourlyReport),
|
|
2261
|
+
buildSupportFactsLine(report),
|
|
2262
|
+
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
2263
|
+
];
|
|
2264
|
+
}
|
|
2265
|
+
function buildPostureLine(report, hourlyReport) {
|
|
2266
|
+
const directActivityMs = Math.max(report.metrics.directActivityMs, 1);
|
|
2267
|
+
const focusRatio = report.metrics.strictEngagementMs / directActivityMs;
|
|
2268
|
+
const agentCoverageRatio = report.metrics.agentCoverageMs / directActivityMs;
|
|
2269
|
+
const quietRatio = sumQuietMs(hourlyReport) / Math.max(1, report.window.end.getTime() - report.window.start.getTime());
|
|
2270
|
+
const posture = quietRatio >= 0.45 ? "Fragmented day" : agentCoverageRatio >= 0.75 && focusRatio < 0.65 ? "Mostly orchestrating" : focusRatio >= 0.8 ? "Mostly in the loop" : report.metrics.peakConcurrentAgents >= 6 ? "Heavy agent day" : "Balanced day";
|
|
2271
|
+
return `${posture}: ${formatDurationHours(report.metrics.strictEngagementMs)} focused, ${formatDurationHours(report.metrics.agentCoverageMs)} agent live`;
|
|
2272
|
+
}
|
|
2273
|
+
function buildBiggestStoryLine(report, hourlyReport) {
|
|
2274
|
+
const longestQuietRun = findLongestQuietRun(hourlyReport);
|
|
2275
|
+
const peakBurnBucket = hourlyReport.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, hourlyReport.buckets[0]);
|
|
2276
|
+
const quietPhrase = longestQuietRun.durationMs >= 2 * 3600000 ? `long quiet stretch ${describeDayPeriod(longestQuietRun.start, report)}` : "steady rhythm overall";
|
|
2277
|
+
return `Biggest story: ${quietPhrase}, big burn ${describeDayPeriod(peakBurnBucket.start, report)}`;
|
|
2278
|
+
}
|
|
2279
|
+
function buildSupportFactsLine(report) {
|
|
2280
|
+
return `${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent • ${report.metrics.peakConcurrentAgents} peak • ${formatCompactInteger(report.tokenTotals.practicalBurn)} burn`;
|
|
2281
|
+
}
|
|
2282
|
+
function sumQuietMs(hourlyReport) {
|
|
2283
|
+
return hourlyReport.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs), 0);
|
|
2284
|
+
}
|
|
2285
|
+
function findLongestQuietRun(hourlyReport) {
|
|
2286
|
+
let longestQuietRun = {
|
|
2287
|
+
durationMs: 0,
|
|
2288
|
+
start: hourlyReport.buckets[0]?.start ?? new Date(0)
|
|
2289
|
+
};
|
|
2290
|
+
let currentStart = null;
|
|
2291
|
+
let currentDurationMs = 0;
|
|
2292
|
+
for (const bucket of hourlyReport.buckets) {
|
|
2293
|
+
const quietMs = Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs);
|
|
2294
|
+
const isQuietBucket = quietMs >= 30 * 60000;
|
|
2295
|
+
if (isQuietBucket) {
|
|
2296
|
+
currentStart ??= bucket.start;
|
|
2297
|
+
currentDurationMs += quietMs;
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
if (currentStart && currentDurationMs > longestQuietRun.durationMs) {
|
|
2301
|
+
longestQuietRun = {
|
|
2302
|
+
durationMs: currentDurationMs,
|
|
2303
|
+
start: currentStart
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
currentStart = null;
|
|
2307
|
+
currentDurationMs = 0;
|
|
2308
|
+
}
|
|
2309
|
+
if (currentStart && currentDurationMs > longestQuietRun.durationMs) {
|
|
2310
|
+
longestQuietRun = {
|
|
2311
|
+
durationMs: currentDurationMs,
|
|
2312
|
+
start: currentStart
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
if (longestQuietRun.durationMs > 0) {
|
|
2316
|
+
return longestQuietRun;
|
|
2317
|
+
}
|
|
2318
|
+
const quietestBucket = hourlyReport.buckets.reduce((currentQuietest, bucket) => {
|
|
2319
|
+
const quietMs = Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs);
|
|
2320
|
+
return quietMs > currentQuietest.durationMs ? { durationMs: quietMs, start: bucket.start } : currentQuietest;
|
|
2321
|
+
}, longestQuietRun);
|
|
2322
|
+
return quietestBucket;
|
|
2323
|
+
}
|
|
2324
|
+
function describeDayPeriod(timestamp, report) {
|
|
2325
|
+
const hourOfDay = Number.parseInt(formatHourOfDay(timestamp, report.window), 10);
|
|
2326
|
+
if (hourOfDay >= 21 || hourOfDay < 5) {
|
|
2327
|
+
return "overnight";
|
|
2328
|
+
}
|
|
2329
|
+
if (hourOfDay < 12) {
|
|
2330
|
+
return "this morning";
|
|
2331
|
+
}
|
|
2332
|
+
if (hourOfDay < 17) {
|
|
2333
|
+
return "this afternoon";
|
|
2334
|
+
}
|
|
2335
|
+
return "this evening";
|
|
2336
|
+
}
|
|
1542
2337
|
function formatDurationLabel(durationMs) {
|
|
1543
2338
|
return `${Math.round(durationMs / 60000)}m`;
|
|
1544
2339
|
}
|
|
@@ -1552,6 +2347,9 @@ function renderSnapshotRow(label, primaryText, detailText, role, options) {
|
|
|
1552
2347
|
// src/cli/run-last24h-command.ts
|
|
1553
2348
|
async function runLast24hCommand(command) {
|
|
1554
2349
|
const window = resolveTrailingReportWindow({ durationMs: command.hourlyWindowMs });
|
|
2350
|
+
const bestMetrics = await refreshBestMetrics();
|
|
2351
|
+
await notifyBestEvents(bestMetrics.newBestEvents);
|
|
2352
|
+
await notifyNearBestMetrics(bestMetrics.currentMetrics, bestMetrics.ledger);
|
|
1555
2353
|
const sessions = await readCodexSessions({
|
|
1556
2354
|
windowStart: window.start,
|
|
1557
2355
|
windowEnd: window.end
|
|
@@ -1569,12 +2367,15 @@ async function runLast24hCommand(command) {
|
|
|
1569
2367
|
wakeWindow: command.wakeWindow,
|
|
1570
2368
|
window
|
|
1571
2369
|
});
|
|
1572
|
-
return renderSummaryReport(summaryReport, createRenderOptions(command.shareMode), hourlyReport);
|
|
2370
|
+
return renderSummaryReport(summaryReport, createRenderOptions(command.shareMode), hourlyReport, buildBestPlaque(bestMetrics.ledger));
|
|
1573
2371
|
}
|
|
1574
2372
|
|
|
1575
2373
|
// src/cli/run-today-command.ts
|
|
1576
2374
|
async function runTodayCommand(command) {
|
|
1577
2375
|
const window = resolveTodayReportWindow();
|
|
2376
|
+
const bestMetrics = await refreshBestMetrics();
|
|
2377
|
+
await notifyBestEvents(bestMetrics.newBestEvents);
|
|
2378
|
+
await notifyNearBestMetrics(bestMetrics.currentMetrics, bestMetrics.ledger);
|
|
1578
2379
|
const sessions = await readCodexSessions({
|
|
1579
2380
|
windowStart: window.start,
|
|
1580
2381
|
windowEnd: window.end
|
|
@@ -1585,7 +2386,7 @@ async function runTodayCommand(command) {
|
|
|
1585
2386
|
idleCutoffMs: command.idleCutoffMs,
|
|
1586
2387
|
wakeWindow: command.wakeWindow,
|
|
1587
2388
|
window
|
|
1588
|
-
}), createRenderOptions(command.shareMode));
|
|
2389
|
+
}), createRenderOptions(command.shareMode), undefined, buildBestPlaque(bestMetrics.ledger));
|
|
1589
2390
|
}
|
|
1590
2391
|
|
|
1591
2392
|
// src/cli/run-idletime.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idletime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Visual CLI for Codex focus, token burn, spikes, and idle time from local session logs.",
|
|
5
5
|
"author": "ParkerRex",
|
|
6
6
|
"main": "./dist/idletime.js",
|
|
@@ -44,12 +44,15 @@
|
|
|
44
44
|
"sideEffects": false,
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "bun run src/release/build-package.ts",
|
|
47
|
-
"check:release": "bun run typecheck && bun test && bun run
|
|
47
|
+
"check:release": "bun run build && bun run typecheck && bun test && bun run qa && npm pack --dry-run",
|
|
48
48
|
"dev": "bun run src/cli/idletime-bin.ts",
|
|
49
49
|
"idletime": "bun run src/cli/idletime-bin.ts",
|
|
50
|
-
"pack:dry-run": "npm pack --dry-run",
|
|
50
|
+
"pack:dry-run": "bun run build && npm pack --dry-run",
|
|
51
51
|
"publish:dry-run": "bun run build && bun publish --dry-run --access public",
|
|
52
52
|
"prepublishOnly": "bun run check:release",
|
|
53
|
+
"qa": "bun run qa:gaps && bun run qa:journeys",
|
|
54
|
+
"qa:gaps": "bun run qa/find-gaps.ts",
|
|
55
|
+
"qa:journeys": "bun run qa/run-shell-journeys.ts",
|
|
53
56
|
"test": "bun test",
|
|
54
57
|
"typecheck": "tsc --noEmit"
|
|
55
58
|
},
|