idletime 0.1.0 → 0.1.2

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
@@ -178,6 +178,19 @@ bun run typecheck
178
178
  bun test
179
179
  ```
180
180
 
181
+ Release QA:
182
+
183
+ ```bash
184
+ bun run qa
185
+ ```
186
+
187
+ That QA pass reads:
188
+
189
+ - `qa/data/user-journeys.csv` for installed-binary shell journeys
190
+ - `qa/data/coverage-matrix.csv` for required release coverage rows
191
+
192
+ 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.
193
+
181
194
  ## Release Prep
182
195
 
183
196
  Build the publishable CLI bundle:
@@ -192,6 +205,13 @@ Dry-run the release checks:
192
205
  bun run check:release
193
206
  ```
194
207
 
208
+ `check:release` now runs:
209
+
210
+ - `bun run typecheck`
211
+ - `bun test`
212
+ - `bun run qa`
213
+ - `npm pack --dry-run`
214
+
195
215
  Dry-run the Bun publish flow:
196
216
 
197
217
  ```bash
@@ -206,11 +226,15 @@ bun run pack:dry-run
206
226
 
207
227
  ## GitHub Release Flow
208
228
 
209
- This repo now includes a publish workflow at `.github/workflows/publish.yml`.
229
+ This repo now includes:
230
+
231
+ - `.github/workflows/ci.yml` for push and pull-request release checks
232
+ - `.github/workflows/publish.yml` for the actual npm publish flow
210
233
 
211
234
  What it does:
212
235
 
213
- - runs on manual dispatch or GitHub release publish
236
+ - `ci.yml` runs on pushes to `dev` and `main`, plus pull requests
237
+ - `publish.yml` runs on manual dispatch or GitHub release publish
214
238
  - installs Bun and Node on a GitHub-hosted runner
215
239
  - runs `bun run check:release`
216
240
  - publishes to npm with `npm publish --access public --provenance`
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.0",
5
+ version: "0.1.2",
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 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
52
  "pack:dry-run": "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) {
@@ -1039,33 +1063,130 @@ function shortenPath(pathText, maxLength) {
1039
1063
 
1040
1064
  // src/reporting/render-theme.ts
1041
1065
  var roleStyles = {
1042
- focus: "1;38;2;56;189;248",
1043
- active: "1;38;2;96;165;250",
1044
- agent: "1;38;2;168;85;247",
1045
- idle: "1;38;2;250;204;21",
1046
- burn: "1;38;2;251;146;60",
1047
- frame: "1;38;2;125;211;252",
1048
- heading: "1;38;2;248;250;252",
1049
- muted: "38;2;148;163;184",
1050
- value: "1;38;2;226;232;240"
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"
1051
1076
  };
1052
1077
  function createRenderOptions(shareMode) {
1053
1078
  return {
1054
1079
  colorEnabled: Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined,
1055
- shareMode
1080
+ shareMode,
1081
+ terminalWidth: process.stdout.columns ?? null
1056
1082
  };
1057
1083
  }
1058
1084
  function paint(text, role, options) {
1085
+ return paintAnsi(text, roleStyles[role], options);
1086
+ }
1087
+ function paintAnsi(text, style, options) {
1059
1088
  if (!options.colorEnabled || text.length === 0) {
1060
1089
  return text;
1061
1090
  }
1062
- return `\x1B[${roleStyles[role]}m${text}\x1B[0m`;
1091
+ return `\x1B[${style}m${text}\x1B[0m`;
1063
1092
  }
1064
1093
  function dim(text, options) {
1065
- if (!options.colorEnabled || text.length === 0) {
1066
- return text;
1094
+ return paintAnsi(text, "2", options);
1095
+ }
1096
+ function measureVisibleTextWidth(text) {
1097
+ let visibleWidth = 0;
1098
+ for (let index = 0;index < text.length; index += 1) {
1099
+ if (text[index] === "\x1B" && text[index + 1] === "[") {
1100
+ index += 2;
1101
+ while (index < text.length && text[index] !== "m") {
1102
+ index += 1;
1103
+ }
1104
+ continue;
1105
+ }
1106
+ visibleWidth += 1;
1067
1107
  }
1068
- return `\x1B[2m${text}\x1B[0m`;
1108
+ return visibleWidth;
1109
+ }
1110
+
1111
+ // src/reporting/render-logo-section.ts
1112
+ var baseBackgroundStyle = "48;2;12;15;8";
1113
+ var wordmarkStyle = `${baseBackgroundStyle};1;38;2;247;245;204`;
1114
+ var wordmarkLines = [
1115
+ " ▄▄ ▄▄",
1116
+ "▀▀ ██ ██ ██ ▀▀",
1117
+ "██ ▄████ ██ ▄█▀█▄ ▀██▀▀ ██ ███▄███▄ ▄█▀█▄",
1118
+ "██ ██ ██ ██ ██▄█▀ ██ ██ ██ ██ ██ ██▄█▀",
1119
+ "██▄ ▀████ ██ ▀█▄▄▄ ██ ██▄ ██ ██ ██ ▀█▄▄▄"
1120
+ ];
1121
+ var patternColors = [
1122
+ { 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 }
1128
+ ];
1129
+ var monochromePatternCharacters = ["░", "░", "▒", "▓", "█"];
1130
+ function buildLogoSection(requestedWidth, options) {
1131
+ const wordmarkWidth = Math.max(...wordmarkLines.map((line) => line.length));
1132
+ const sectionWidth = Math.max(requestedWidth, wordmarkWidth);
1133
+ const patternWidth = Math.max(0, sectionWidth - wordmarkWidth);
1134
+ return wordmarkLines.map((line, rowIndex) => {
1135
+ const paddedWordmark = padRight(line, wordmarkWidth);
1136
+ const patternTail = buildPatternTail(patternWidth, rowIndex, options);
1137
+ return `${paintAnsi(paddedWordmark, wordmarkStyle, options)}${patternTail}`;
1138
+ });
1139
+ }
1140
+ function resolveLogoSectionWidth(minimumWidth, options) {
1141
+ return Math.max(minimumWidth, options.terminalWidth ?? 0);
1142
+ }
1143
+ function buildPatternTail(width, rowIndex, options) {
1144
+ if (!options.colorEnabled) {
1145
+ return buildMonochromePatternTail(width, rowIndex);
1146
+ }
1147
+ let patternTail = "";
1148
+ let currentStyle = "";
1149
+ let currentSegmentWidth = 0;
1150
+ for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1151
+ const style = getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex));
1152
+ if (style === currentStyle) {
1153
+ currentSegmentWidth += 1;
1154
+ continue;
1155
+ }
1156
+ if (currentSegmentWidth > 0) {
1157
+ patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1158
+ }
1159
+ currentStyle = style;
1160
+ currentSegmentWidth = 1;
1161
+ }
1162
+ if (currentSegmentWidth > 0) {
1163
+ patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1164
+ }
1165
+ return patternTail;
1166
+ }
1167
+ function buildMonochromePatternTail(width, rowIndex) {
1168
+ let patternTail = "";
1169
+ for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1170
+ const intensity = getPatternIntensity(width, rowIndex, columnIndex);
1171
+ const characterIndex = Math.min(monochromePatternCharacters.length - 1, Math.floor(intensity * monochromePatternCharacters.length));
1172
+ patternTail += monochromePatternCharacters[characterIndex];
1173
+ }
1174
+ return patternTail;
1175
+ }
1176
+ function getPatternIntensity(width, rowIndex, columnIndex) {
1177
+ const normalizedColumn = width <= 1 ? 0 : columnIndex / Math.max(1, width - 1);
1178
+ const envelope = 0.18 + 0.82 * Math.pow(normalizedColumn, 0.82);
1179
+ const wave = Math.sin((columnIndex + rowIndex * 1.9) / 2.9) * 0.22 + Math.cos((columnIndex - rowIndex * 2.7) / 6.3) * 0.18 + Math.sin((columnIndex + rowIndex * 3.4) / 10.5) * 0.12;
1180
+ const blockOffset = ((Math.floor(columnIndex / 2) + rowIndex) % 2 === 0 ? 0.06 : -0.04) + (rowIndex === 0 ? -0.06 : 0);
1181
+ return clamp(envelope + wave + blockOffset, 0, 1);
1182
+ }
1183
+ function getPatternCellStyle(intensity) {
1184
+ const colorIndex = Math.min(patternColors.length - 1, Math.floor(intensity * patternColors.length));
1185
+ const color = patternColors[colorIndex];
1186
+ return `48;2;${color.red};${color.green};${color.blue}`;
1187
+ }
1188
+ function clamp(value, minValue, maxValue) {
1189
+ return Math.min(maxValue, Math.max(minValue, value));
1069
1190
  }
1070
1191
 
1071
1192
  // src/reporting/render-layout.ts
@@ -1095,32 +1216,39 @@ function renderSectionTitle(title, options) {
1095
1216
  }
1096
1217
 
1097
1218
  // src/reporting/render-rhythm-section.ts
1219
+ var groupSize = 4;
1098
1220
  function buildRhythmSection(report, options) {
1099
1221
  const quietValues = report.buckets.map((bucket) => Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs));
1100
1222
  const idleValues = report.hasWakeWindow ? report.buckets.map((bucket) => bucket.awakeIdleMs) : quietValues;
1101
1223
  const idleLabel = report.hasWakeWindow ? "idle" : "quiet";
1102
- return [
1224
+ const idleTotal = formatDurationCompact(idleValues.reduce((totalDurationMs, idleDurationMs) => totalDurationMs + idleDurationMs, 0));
1225
+ const lines = [
1103
1226
  ...renderSectionTitle("24h Rhythm", options),
1104
1227
  paint(` hours ${buildHourMarkerLine(report)}`, "muted", options),
1105
- renderRhythmRow("focus", buildSparkline(report.buckets.map((bucket) => bucket.engagedMs)), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.engagedMs, 0)), "focus", options),
1106
- renderRhythmRow("active", buildSparkline(report.buckets.map((bucket) => bucket.directActivityMs)), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.directActivityMs, 0)), "active", options),
1107
- renderRhythmRow(padRight(idleLabel, 6).trimEnd(), buildSparkline(idleValues), formatDurationCompact(idleValues.reduce((totalDurationMs, idleDurationMs) => totalDurationMs + idleDurationMs, 0)), "idle", options),
1108
- renderRhythmRow("burn", buildSparkline(report.buckets.map((bucket) => bucket.practicalBurn)), formatCompactInteger(report.buckets.reduce((totalBurn, bucket) => totalBurn + bucket.practicalBurn, 0)), "burn", options)
1228
+ renderRhythmRow("focus", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.engagedMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.engagedMs, 0)), "focus", options),
1229
+ renderRhythmRow("active", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.directActivityMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.directActivityMs, 0)), "active", options)
1109
1230
  ];
1231
+ lines.push(renderRhythmRow(padRight(idleLabel, 6).trimEnd(), buildGroupedTrack(buildSparkline(idleValues)), idleTotal, "idle", options));
1232
+ lines.push(renderRhythmRow("burn", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.practicalBurn))), formatCompactInteger(report.buckets.reduce((totalBurn, bucket) => totalBurn + bucket.practicalBurn, 0)), "burn", options));
1233
+ return lines;
1234
+ }
1235
+ function buildGroupedTrack(text) {
1236
+ const groups = [];
1237
+ for (let i = 0;i < text.length; i += groupSize) {
1238
+ groups.push(text.slice(i, i + groupSize));
1239
+ }
1240
+ return groups.join("│");
1110
1241
  }
1111
1242
  function buildHourMarkerLine(report) {
1112
- const markerCharacters = Array.from({ length: report.buckets.length }, () => " ");
1113
- for (const [index, bucket] of report.buckets.entries()) {
1114
- if (index % 4 !== 0) {
1243
+ const markerGroups = [];
1244
+ for (let index = 0;index < report.buckets.length; index += groupSize) {
1245
+ const bucket = report.buckets[index];
1246
+ if (!bucket) {
1115
1247
  continue;
1116
1248
  }
1117
- const hourLabel = formatHourOfDay(bucket.start, report.window);
1118
- markerCharacters[index] = hourLabel[0] ?? " ";
1119
- if (index + 1 < markerCharacters.length) {
1120
- markerCharacters[index + 1] = hourLabel[1] ?? " ";
1121
- }
1249
+ markerGroups.push(padRight(formatHourOfDay(bucket.start, report.window), Math.min(groupSize, report.buckets.length - index)));
1122
1250
  }
1123
- return markerCharacters.join("");
1251
+ return markerGroups.join("");
1124
1252
  }
1125
1253
  function renderRhythmRow(label, sparkline, totalText, role, options) {
1126
1254
  return `${paint(` ${padRight(label, 6)}`, role, options)} ${paint(sparkline, role, options)} ${paint(totalText, "value", options)}`;
@@ -1147,11 +1275,16 @@ function renderFullHourlyReport(report, options) {
1147
1275
  const lines = [];
1148
1276
  const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
1149
1277
  const peakFocusBucket = report.buckets.reduce((currentPeak, bucket) => bucket.engagedMs > currentPeak.engagedMs ? bucket : currentPeak, report.buckets[0]);
1150
- lines.push(...renderPanel(`idletime hourly • ${report.window.label}`, [
1278
+ const panelLines = renderPanel(`idletime hourly • ${report.window.label}`, [
1151
1279
  `${formatTimestamp(report.window.start, report.window)} -> ${formatTimestamp(report.window.end, report.window)}`,
1152
1280
  buildFilterLine(report),
1153
1281
  `peaks burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourBucketLabel(peakBurnBucket.start, report.window)} • focus ${formatDurationCompact(peakFocusBucket.engagedMs)} @ ${formatHourBucketLabel(peakFocusBucket.start, report.window)} • concurrency ${Math.max(...report.buckets.map((bucket) => bucket.peakConcurrentAgents), 0)}`
1154
- ], options));
1282
+ ], options);
1283
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1284
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1285
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1286
+ lines.push("");
1287
+ lines.push(...panelLines);
1155
1288
  lines.push("");
1156
1289
  lines.push(...buildRhythmSection(report, options));
1157
1290
  lines.push("");
@@ -1171,11 +1304,16 @@ function renderFullHourlyReport(report, options) {
1171
1304
  function renderShareHourlyReport(report, options) {
1172
1305
  const lines = [];
1173
1306
  const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
1174
- lines.push(...renderPanel(`idletime hourly • ${report.window.label}`, [
1307
+ const panelLines = renderPanel(`idletime hourly • ${report.window.label}`, [
1175
1308
  `${formatTimestamp(report.window.start, report.window)} -> ${formatTimestamp(report.window.end, report.window)}`,
1176
1309
  buildFilterLine(report),
1177
1310
  `peak burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourBucketLabel(peakBurnBucket.start, report.window)}`
1178
- ], options));
1311
+ ], options);
1312
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1313
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1314
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1315
+ lines.push("");
1316
+ lines.push(...panelLines);
1179
1317
  lines.push("");
1180
1318
  lines.push(...buildRhythmSection(report, options));
1181
1319
  lines.push("");
@@ -1324,19 +1462,18 @@ function renderFullSummaryReport(report, options, hourlyReport) {
1324
1462
  const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
1325
1463
  const headerLines = [
1326
1464
  formatTimeRange(report.window.start, report.window.end, report.window),
1327
- report.activityWindow ? `active ${formatTimeRange(report.activityWindow.start, report.activityWindow.end, report.window)}` : "active no matching sessions",
1328
- buildHeaderMeta(report),
1465
+ `${report.sessionCounts.total} sessions · ${formatDurationHours(requestedMetrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
1329
1466
  ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
1330
1467
  ];
1331
- if (hourlyReport) {
1332
- headerLines.push(buildPeakLine(hourlyReport));
1333
- }
1334
- lines.push(...renderPanel(`idletime • ${report.window.label}`, headerLines, options));
1468
+ const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
1469
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1470
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1471
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1472
+ lines.push("");
1473
+ lines.push(...panelLines);
1335
1474
  if (hourlyReport) {
1336
1475
  lines.push("");
1337
1476
  lines.push(...buildRhythmSection(hourlyReport, options));
1338
- lines.push("");
1339
- lines.push(...buildSpikeSection(hourlyReport, options));
1340
1477
  }
1341
1478
  lines.push("");
1342
1479
  lines.push(...renderSectionTitle("Activity", options));
@@ -1358,10 +1495,10 @@ function renderFullSummaryReport(report, options, hourlyReport) {
1358
1495
  lines.push(...renderSectionTitle("Tokens", options));
1359
1496
  const maxBurnValue = Math.max(report.tokenTotals.practicalBurn, report.directTokenTotals.practicalBurn);
1360
1497
  const maxRawValue = Math.max(report.tokenTotals.rawTotalTokens, report.directTokenTotals.rawTotalTokens);
1361
- lines.push(renderMetricRow("practical burn", report.tokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "█", "burn", options));
1362
- lines.push(renderMetricRow("all raw", report.tokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.tokenTotals.rawTotalTokens), `${formatInteger(report.tokenTotals.rawTotalTokens)} total`, "", "burn", options));
1363
- lines.push(renderMetricRow("direct burn", report.directTokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.directTokenTotals.practicalBurn), `${formatPercentage(report.directTokenTotals.practicalBurn / report.tokenTotals.practicalBurn)} of burn`, "▒", "burn", options));
1364
- lines.push(renderMetricRow("direct raw", report.directTokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.directTokenTotals.rawTotalTokens), `${formatPercentage(report.directTokenTotals.rawTotalTokens / report.tokenTotals.rawTotalTokens)} of raw`, "", "burn", options));
1498
+ lines.push(renderMetricRow("practical burn", report.tokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "█", "burn", options, "burn"));
1499
+ lines.push(renderMetricRow("all raw", report.tokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.tokenTotals.rawTotalTokens), `${formatInteger(report.tokenTotals.rawTotalTokens)} total`, "", "raw", options, "raw"));
1500
+ lines.push(renderMetricRow("direct burn", report.directTokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.directTokenTotals.practicalBurn), `${formatPercentage(report.directTokenTotals.practicalBurn / report.tokenTotals.practicalBurn)} of burn`, "▒", "burn", options, "burn"));
1501
+ lines.push(renderMetricRow("direct raw", report.directTokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.directTokenTotals.rawTotalTokens), `${formatPercentage(report.directTokenTotals.rawTotalTokens / report.tokenTotals.rawTotalTokens)} of raw`, "", "raw", options, "raw"));
1365
1502
  if (report.wakeSummary) {
1366
1503
  lines.push("");
1367
1504
  lines.push(...renderSectionTitle("Wake Window", options));
@@ -1386,19 +1523,18 @@ function renderShareSummaryReport(report, options, hourlyReport) {
1386
1523
  const lines = [];
1387
1524
  const headerLines = [
1388
1525
  formatTimeRange(report.window.start, report.window.end, report.window),
1389
- report.activityWindow ? `active ${formatTimeRange(report.activityWindow.start, report.activityWindow.end, report.window)}` : "active no matching sessions",
1390
- buildHeaderMeta(report),
1526
+ `${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
1391
1527
  ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
1392
1528
  ];
1393
- if (hourlyReport) {
1394
- headerLines.push(buildPeakLine(hourlyReport));
1395
- }
1396
- lines.push(...renderPanel(`idletime • ${report.window.label}`, headerLines, options));
1529
+ const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
1530
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1531
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1532
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1533
+ lines.push("");
1534
+ lines.push(...panelLines);
1397
1535
  if (hourlyReport) {
1398
1536
  lines.push("");
1399
1537
  lines.push(...buildRhythmSection(hourlyReport, options));
1400
- lines.push("");
1401
- lines.push(...buildSpikeSection(hourlyReport, options));
1402
1538
  }
1403
1539
  lines.push("");
1404
1540
  lines.push(...renderSectionTitle("Snapshot", options));
@@ -1430,16 +1566,8 @@ function formatAppliedFilters(report) {
1430
1566
  function formatDurationLabel(durationMs) {
1431
1567
  return `${Math.round(durationMs / 60000)}m`;
1432
1568
  }
1433
- function buildHeaderMeta(report) {
1434
- return `${report.sessionCounts.total} sessions ${report.sessionCounts.direct} direct ${report.sessionCounts.subagent} subagent peak ${report.metrics.peakConcurrentAgents} agents`;
1435
- }
1436
- function buildPeakLine(report) {
1437
- const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
1438
- const peakFocusBucket = report.buckets.reduce((currentPeak, bucket) => bucket.engagedMs > currentPeak.engagedMs ? bucket : currentPeak, report.buckets[0]);
1439
- return `peaks burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourOfDay(peakBurnBucket.start, report.window)} • focus ${formatDurationCompact(peakFocusBucket.engagedMs)} @ ${formatHourOfDay(peakFocusBucket.start, report.window)}`;
1440
- }
1441
- function renderMetricRow(label, value, maxValue, primaryText, detailText, filledCharacter, role, options) {
1442
- return `${paint(padRight(` ${label}`, 14), "muted", options)} ${paint(buildBar(value, maxValue, summaryBarWidth, filledCharacter), role, options)} ${paint(padRight(primaryText, 7), "value", options)} ${dim(detailText, options)}`;
1569
+ function renderMetricRow(label, value, maxValue, primaryText, detailText, filledCharacter, role, options, valueRole = "value") {
1570
+ return `${paint(padRight(` ${label}`, 14), "muted", options)} ${paint(buildBar(value, maxValue, summaryBarWidth, filledCharacter), role, options)} ${paint(padRight(primaryText, 7), valueRole, options)} ${dim(detailText, options)}`;
1443
1571
  }
1444
1572
  function renderSnapshotRow(label, primaryText, detailText, role, options) {
1445
1573
  return `${paint(padRight(` ${label}`, 12), role, options)} ${paint(padRight(primaryText, 10), "value", options)} ${dim(detailText, options)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idletime",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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 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
50
  "pack:dry-run": "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
  },