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 a publish workflow at `.github/workflows/publish.yml`.
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 manual dispatch or GitHub release publish
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`
@@ -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.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 build && npm pack --dry-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
- throw new Error(`Token usage regressed for ${key}.`);
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: previousPoint ? subtractTokenUsages(tokenPoint.usage, previousPoint.usage) : tokenPoint.usage
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;236;239;148",
1043
- active: "1;38;2;208;219;96",
1044
- agent: "1;38;2;166;182;77",
1045
- idle: "1;38;2;138;150;66",
1046
- burn: "1;38;2;228;209;92",
1047
- raw: "1;38;2;188;172;80",
1048
- frame: "1;38;2;149;158;56",
1049
- heading: "1;38;2;249;246;212",
1050
- muted: "38;2;142;145;96",
1051
- value: "1;38;2;242;236;179"
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 wordmarkStyle = `${baseBackgroundStyle};1;38;2;247;245;204`;
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: 48, green: 58, blue: 18 },
1100
- { red: 86, green: 96, blue: 24 },
1101
- { red: 128, green: 138, blue: 30 },
1102
- { red: 176, green: 188, blue: 40 },
1103
- { red: 220, green: 228, blue: 78 }
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 currentSegmentWidth = 0;
1202
+ let currentSegment = "";
1126
1203
  for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1127
- const style = getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex));
1204
+ const overlayCharacter = overlayCharacters[columnIndex];
1205
+ const style = overlayCharacter === null ? cellStyles[columnIndex] : `${cellStyles[columnIndex]};${plaqueTextStyle}`;
1128
1206
  if (style === currentStyle) {
1129
- currentSegmentWidth += 1;
1207
+ currentSegment += overlayCharacter ?? " ";
1130
1208
  continue;
1131
1209
  }
1132
- if (currentSegmentWidth > 0) {
1133
- patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1210
+ if (currentSegment.length > 0) {
1211
+ patternTail += paintAnsi(currentSegment, currentStyle, options);
1134
1212
  }
1135
1213
  currentStyle = style;
1136
- currentSegmentWidth = 1;
1214
+ currentSegment = overlayCharacter ?? " ";
1137
1215
  }
1138
- if (currentSegmentWidth > 0) {
1139
- patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
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.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 build && npm pack --dry-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
  },