idletime 0.1.2 → 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
@@ -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.2",
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,10 +46,10 @@ 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 qa && 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
55
  qa: "bun run qa:gaps && bun run qa:journeys",
@@ -1061,18 +1061,66 @@ function shortenPath(pathText, maxLength) {
1061
1061
  return `...${shortenedPath || pathText.slice(-(maxLength - 3))}`;
1062
1062
  }
1063
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
+
1064
1112
  // src/reporting/render-theme.ts
1065
1113
  var roleStyles = {
1066
- focus: "1;38;2;236;239;148",
1067
- active: "1;38;2;208;219;96",
1068
- agent: "1;38;2;166;182;77",
1069
- idle: "1;38;2;138;150;66",
1070
- burn: "1;38;2;228;209;92",
1071
- raw: "1;38;2;188;172;80",
1072
- frame: "1;38;2;149;158;56",
1073
- heading: "1;38;2;249;246;212",
1074
- muted: "38;2;142;145;96",
1075
- 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"
1076
1124
  };
1077
1125
  function createRenderOptions(shareMode) {
1078
1126
  return {
@@ -1110,7 +1158,9 @@ function measureVisibleTextWidth(text) {
1110
1158
 
1111
1159
  // src/reporting/render-logo-section.ts
1112
1160
  var baseBackgroundStyle = "48;2;12;15;8";
1113
- 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`;
1114
1164
  var wordmarkLines = [
1115
1165
  " ▄▄ ▄▄",
1116
1166
  "▀▀ ██ ██ ██ ▀▀",
@@ -1120,59 +1170,76 @@ var wordmarkLines = [
1120
1170
  ];
1121
1171
  var patternColors = [
1122
1172
  { red: 20, green: 24, blue: 10 },
1123
- { red: 48, green: 58, blue: 18 },
1124
- { red: 86, green: 96, blue: 24 },
1125
- { red: 128, green: 138, blue: 30 },
1126
- { red: 176, green: 188, blue: 40 },
1127
- { 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 }
1128
1178
  ];
1129
1179
  var monochromePatternCharacters = ["░", "░", "▒", "▓", "█"];
1130
- function buildLogoSection(requestedWidth, options) {
1180
+ function buildLogoSection(requestedWidth, options, bestPlaque = null) {
1131
1181
  const wordmarkWidth = Math.max(...wordmarkLines.map((line) => line.length));
1132
1182
  const sectionWidth = Math.max(requestedWidth, wordmarkWidth);
1133
1183
  const patternWidth = Math.max(0, sectionWidth - wordmarkWidth);
1184
+ const plaqueRows = bestPlaque ? buildBestPlaqueRows(bestPlaque, Math.max(0, patternWidth - plaqueInsetColumns)) : null;
1134
1185
  return wordmarkLines.map((line, rowIndex) => {
1135
1186
  const paddedWordmark = padRight(line, wordmarkWidth);
1136
- const patternTail = buildPatternTail(patternWidth, rowIndex, options);
1187
+ const patternTail = buildPatternTail(patternWidth, rowIndex, options, plaqueRows?.[rowIndex] ?? "");
1137
1188
  return `${paintAnsi(paddedWordmark, wordmarkStyle, options)}${patternTail}`;
1138
1189
  });
1139
1190
  }
1140
1191
  function resolveLogoSectionWidth(minimumWidth, options) {
1141
1192
  return Math.max(minimumWidth, options.terminalWidth ?? 0);
1142
1193
  }
1143
- function buildPatternTail(width, rowIndex, options) {
1194
+ function buildPatternTail(width, rowIndex, options, plaqueRowText) {
1144
1195
  if (!options.colorEnabled) {
1145
- return buildMonochromePatternTail(width, rowIndex);
1196
+ return buildMonochromePatternTail(width, rowIndex, plaqueRowText);
1146
1197
  }
1198
+ const overlayCharacters = createOverlayCharacters(width, plaqueRowText);
1199
+ const cellStyles = Array.from({ length: width }, (_, columnIndex) => getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex)));
1147
1200
  let patternTail = "";
1148
1201
  let currentStyle = "";
1149
- let currentSegmentWidth = 0;
1202
+ let currentSegment = "";
1150
1203
  for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1151
- const style = getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex));
1204
+ const overlayCharacter = overlayCharacters[columnIndex];
1205
+ const style = overlayCharacter === null ? cellStyles[columnIndex] : `${cellStyles[columnIndex]};${plaqueTextStyle}`;
1152
1206
  if (style === currentStyle) {
1153
- currentSegmentWidth += 1;
1207
+ currentSegment += overlayCharacter ?? " ";
1154
1208
  continue;
1155
1209
  }
1156
- if (currentSegmentWidth > 0) {
1157
- patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1210
+ if (currentSegment.length > 0) {
1211
+ patternTail += paintAnsi(currentSegment, currentStyle, options);
1158
1212
  }
1159
1213
  currentStyle = style;
1160
- currentSegmentWidth = 1;
1214
+ currentSegment = overlayCharacter ?? " ";
1161
1215
  }
1162
- if (currentSegmentWidth > 0) {
1163
- patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1216
+ if (currentSegment.length > 0) {
1217
+ patternTail += paintAnsi(currentSegment, currentStyle, options);
1164
1218
  }
1165
1219
  return patternTail;
1166
1220
  }
1167
- function buildMonochromePatternTail(width, rowIndex) {
1221
+ function buildMonochromePatternTail(width, rowIndex, plaqueRowText) {
1222
+ const overlayCharacters = createOverlayCharacters(width, plaqueRowText);
1168
1223
  let patternTail = "";
1169
1224
  for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1225
+ const overlayCharacter = overlayCharacters[columnIndex];
1226
+ if (overlayCharacter !== null) {
1227
+ patternTail += overlayCharacter;
1228
+ continue;
1229
+ }
1170
1230
  const intensity = getPatternIntensity(width, rowIndex, columnIndex);
1171
1231
  const characterIndex = Math.min(monochromePatternCharacters.length - 1, Math.floor(intensity * monochromePatternCharacters.length));
1172
1232
  patternTail += monochromePatternCharacters[characterIndex];
1173
1233
  }
1174
1234
  return patternTail;
1175
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
+ }
1176
1243
  function getPatternIntensity(width, rowIndex, columnIndex) {
1177
1244
  const normalizedColumn = width <= 1 ? 0 : columnIndex / Math.max(1, width - 1);
1178
1245
  const envelope = 0.18 + 0.82 * Math.pow(normalizedColumn, 0.82);
@@ -1355,6 +1422,631 @@ async function runHourlyCommand(command) {
1355
1422
  }), createRenderOptions(command.shareMode));
1356
1423
  }
1357
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
+
1358
2050
  // src/reporting/build-summary-report.ts
1359
2051
  function buildSummaryReport(sessions, query) {
1360
2052
  const filteredSessions = filterSessions(sessions, query.filters);
@@ -1452,23 +2144,19 @@ function clipActivityMetricsToWindow(metrics, windowInterval) {
1452
2144
 
1453
2145
  // src/reporting/render-summary-report.ts
1454
2146
  var summaryBarWidth = 18;
1455
- function renderSummaryReport(report, options, hourlyReport) {
1456
- 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);
1457
2149
  }
1458
- function renderFullSummaryReport(report, options, hourlyReport) {
2150
+ function renderFullSummaryReport(report, options, hourlyReport, bestPlaque = null) {
1459
2151
  const lines = [];
1460
2152
  const requestedMetrics = report.metrics;
1461
2153
  const actualComparisonMetrics = report.comparisonMetrics;
1462
2154
  const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
1463
- const headerLines = [
1464
- formatTimeRange(report.window.start, report.window.end, report.window),
1465
- `${report.sessionCounts.total} sessions · ${formatDurationHours(requestedMetrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
1466
- ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
1467
- ];
2155
+ const headerLines = buildSummaryHeaderLines(report, hourlyReport);
1468
2156
  const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
1469
2157
  const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1470
2158
  const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1471
- lines.push(...buildLogoSection(logoSectionWidth, options));
2159
+ lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
1472
2160
  lines.push("");
1473
2161
  lines.push(...panelLines);
1474
2162
  if (hourlyReport) {
@@ -1519,17 +2207,13 @@ function renderFullSummaryReport(report, options, hourlyReport) {
1519
2207
  return lines.join(`
1520
2208
  `);
1521
2209
  }
1522
- function renderShareSummaryReport(report, options, hourlyReport) {
2210
+ function renderShareSummaryReport(report, options, hourlyReport, bestPlaque = null) {
1523
2211
  const lines = [];
1524
- const headerLines = [
1525
- formatTimeRange(report.window.start, report.window.end, report.window),
1526
- `${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
1527
- ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
1528
- ];
2212
+ const headerLines = buildSummaryHeaderLines(report, hourlyReport);
1529
2213
  const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
1530
2214
  const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1531
2215
  const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1532
- lines.push(...buildLogoSection(logoSectionWidth, options));
2216
+ lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
1533
2217
  lines.push("");
1534
2218
  lines.push(...panelLines);
1535
2219
  if (hourlyReport) {
@@ -1563,6 +2247,93 @@ function formatAppliedFilters(report) {
1563
2247
  }
1564
2248
  return appliedFilters;
1565
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
+ }
1566
2337
  function formatDurationLabel(durationMs) {
1567
2338
  return `${Math.round(durationMs / 60000)}m`;
1568
2339
  }
@@ -1576,6 +2347,9 @@ function renderSnapshotRow(label, primaryText, detailText, role, options) {
1576
2347
  // src/cli/run-last24h-command.ts
1577
2348
  async function runLast24hCommand(command) {
1578
2349
  const window = resolveTrailingReportWindow({ durationMs: command.hourlyWindowMs });
2350
+ const bestMetrics = await refreshBestMetrics();
2351
+ await notifyBestEvents(bestMetrics.newBestEvents);
2352
+ await notifyNearBestMetrics(bestMetrics.currentMetrics, bestMetrics.ledger);
1579
2353
  const sessions = await readCodexSessions({
1580
2354
  windowStart: window.start,
1581
2355
  windowEnd: window.end
@@ -1593,12 +2367,15 @@ async function runLast24hCommand(command) {
1593
2367
  wakeWindow: command.wakeWindow,
1594
2368
  window
1595
2369
  });
1596
- return renderSummaryReport(summaryReport, createRenderOptions(command.shareMode), hourlyReport);
2370
+ return renderSummaryReport(summaryReport, createRenderOptions(command.shareMode), hourlyReport, buildBestPlaque(bestMetrics.ledger));
1597
2371
  }
1598
2372
 
1599
2373
  // src/cli/run-today-command.ts
1600
2374
  async function runTodayCommand(command) {
1601
2375
  const window = resolveTodayReportWindow();
2376
+ const bestMetrics = await refreshBestMetrics();
2377
+ await notifyBestEvents(bestMetrics.newBestEvents);
2378
+ await notifyNearBestMetrics(bestMetrics.currentMetrics, bestMetrics.ledger);
1602
2379
  const sessions = await readCodexSessions({
1603
2380
  windowStart: window.start,
1604
2381
  windowEnd: window.end
@@ -1609,7 +2386,7 @@ async function runTodayCommand(command) {
1609
2386
  idleCutoffMs: command.idleCutoffMs,
1610
2387
  wakeWindow: command.wakeWindow,
1611
2388
  window
1612
- }), createRenderOptions(command.shareMode));
2389
+ }), createRenderOptions(command.shareMode), undefined, buildBestPlaque(bestMetrics.ledger));
1613
2390
  }
1614
2391
 
1615
2392
  // src/cli/run-idletime.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idletime",
3
- "version": "0.1.2",
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,10 +44,10 @@
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 qa && 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
53
  "qa": "bun run qa:gaps && bun run qa:journeys",