@ekkos/cli 1.3.0 → 1.3.1

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.
@@ -1089,14 +1089,15 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1089
1089
  let header = '';
1090
1090
  let separator = '';
1091
1091
  let rows = [];
1092
- // Always show all 7 columns: Turn, Model, Context, Cache Rd, Cache Wr, Output, Cost
1092
+ // Always show all 8 columns: Turn, Time, Model, Context, Cache Rd, Cache Wr, Output, Cost
1093
1093
  // Shrink flex columns to fit narrow panes instead of dropping them.
1094
1094
  const colNum = 4;
1095
+ const colTime = 8; // "HH:MM:SS" or "H:MM AM"
1095
1096
  const colM = 7;
1096
1097
  const colCtx = 7;
1097
1098
  const colCost = 8;
1098
- const nDividers = 6;
1099
- const fixedW = colNum + colM + colCtx + colCost;
1099
+ const nDividers = 7;
1100
+ const fixedW = colNum + colTime + colM + colCtx + colCost;
1100
1101
  const flexTotal = Math.max(0, w - fixedW - nDividers);
1101
1102
  let rdW = Math.max(5, Math.floor(flexTotal * 0.35));
1102
1103
  let wrW = Math.max(5, Math.floor(flexTotal * 0.30));
@@ -1118,14 +1119,30 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1118
1119
  rdW -= trimRd;
1119
1120
  }
1120
1121
  }
1121
- header = `{bold}${pad('Turn', colNum)}${div}${pad('Model', colM)}${div}${pad('Context', colCtx)}${div}${rpad('Cache Rd', rdW)}${div}${rpad('Cache Wr', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}{/bold}`;
1122
- separator = `{gray-fg}${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}{/gray-fg}`;
1122
+ // Format turn timestamp to short time string
1123
+ const fmtTime = (iso) => {
1124
+ if (!iso)
1125
+ return '--:--';
1126
+ try {
1127
+ const d = new Date(iso);
1128
+ const h = d.getHours();
1129
+ const m = d.getMinutes().toString().padStart(2, '0');
1130
+ const s = d.getSeconds().toString().padStart(2, '0');
1131
+ return `${h}:${m}:${s}`;
1132
+ }
1133
+ catch {
1134
+ return '--:--';
1135
+ }
1136
+ };
1137
+ header = `{bold}${pad('Turn', colNum)}${div}${pad('Time', colTime)}${div}${pad('Model', colM)}${div}${pad('Context', colCtx)}${div}${rpad('Cache Rd', rdW)}${div}${rpad('Cache Wr', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}{/bold}`;
1138
+ separator = `{gray-fg}${'─'.repeat(colNum)}┼${'─'.repeat(colTime)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}{/gray-fg}`;
1123
1139
  rows = turns.map(t => {
1124
1140
  const mTag = modelTag(t.routedModel);
1125
1141
  const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
1126
1142
  const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
1127
1143
  const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
1128
1144
  return (pad(String(t.turn), colNum) + div +
1145
+ `{gray-fg}${pad(fmtTime(t.timestamp), colTime)}{/gray-fg}` + div +
1129
1146
  `{${mColor}-fg}${pad(mTag, colM)}{/${mColor}-fg}` + div +
1130
1147
  pad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
1131
1148
  `{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
@@ -1220,9 +1237,22 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1220
1237
  return null;
1221
1238
  }
1222
1239
  }
1223
- async function updateWindowBox() {
1240
+ // Cache last fetched usage data so the countdown can tick every second
1241
+ let cachedUsage = null;
1242
+ // Fetch fresh usage data from API (called every 15s)
1243
+ async function fetchAndCacheUsage() {
1244
+ try {
1245
+ cachedUsage = await fetchAnthropicUsage();
1246
+ }
1247
+ catch (err) {
1248
+ dlog(`Window fetch: ${err.message}`);
1249
+ }
1250
+ renderWindowBox();
1251
+ }
1252
+ // Render countdown from cached data (called every 1s)
1253
+ function renderWindowBox() {
1224
1254
  try {
1225
- const usage = await fetchAnthropicUsage();
1255
+ const usage = cachedUsage;
1226
1256
  let line1 = ' {gray-fg}No usage data{/gray-fg}';
1227
1257
  let line2 = '';
1228
1258
  // ── 5h Window (from Anthropic OAuth API) ──
@@ -1230,26 +1260,38 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1230
1260
  const pct = usage.five_hour.utilization;
1231
1261
  const resetAt = new Date(usage.five_hour.resets_at).getTime();
1232
1262
  const remainMs = Math.max(0, resetAt - Date.now());
1233
- const remainMin = Math.round(remainMs / 60000);
1234
- const rH = Math.floor(remainMin / 60);
1235
- const rM = remainMin % 60;
1263
+ const remainSec = Math.round(remainMs / 1000);
1264
+ const rH = Math.floor(remainSec / 3600);
1265
+ const rM = Math.floor((remainSec % 3600) / 60);
1266
+ const rS = remainSec % 60;
1236
1267
  const pctColor = pct < 50 ? 'green' : pct < 80 ? 'yellow' : 'red';
1237
- const timeColor = remainMin > 120 ? 'green' : remainMin > 60 ? 'yellow' : 'red';
1268
+ const countdown = `${rH}h${rM.toString().padStart(2, '0')}m${rS.toString().padStart(2, '0')}s`;
1269
+ const resetDate = new Date(resetAt);
1270
+ const resetTime = resetDate.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
1238
1271
  line1 = ` {bold}5h:{/bold}` +
1239
1272
  ` {${pctColor}-fg}${pct.toFixed(0)}% used{/${pctColor}-fg}` +
1240
- ` {${timeColor}-fg}${rH}h${rM}m left{/${timeColor}-fg}`;
1273
+ ` {${pctColor}-fg}${countdown}{/${pctColor}-fg}` +
1274
+ ` resets ${resetTime}`;
1241
1275
  }
1242
1276
  // ── Weekly (from Anthropic OAuth API) ──
1243
1277
  if (usage?.seven_day) {
1244
1278
  const pct = usage.seven_day.utilization;
1245
1279
  const resetAt = new Date(usage.seven_day.resets_at);
1246
- const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1247
- const resetDay = dayNames[resetAt.getDay()];
1248
- const resetHour = resetAt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
1280
+ const remainMs = Math.max(0, resetAt.getTime() - Date.now());
1281
+ const remainSec = Math.round(remainMs / 1000);
1282
+ const rD = Math.floor(remainSec / 86400);
1283
+ const rH = Math.floor((remainSec % 86400) / 3600);
1284
+ const rM = Math.floor((remainSec % 3600) / 60);
1285
+ const rS = remainSec % 60;
1249
1286
  const pctColor = pct < 50 ? 'green' : pct < 80 ? 'yellow' : 'red';
1287
+ const countdown = rD > 0
1288
+ ? `${rD}d${rH}h${rM.toString().padStart(2, '0')}m`
1289
+ : `${rH}h${rM.toString().padStart(2, '0')}m${rS.toString().padStart(2, '0')}s`;
1290
+ const resetTime = resetAt.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
1250
1291
  line2 = ` {bold}Week:{/bold}` +
1251
1292
  ` {${pctColor}-fg}${pct.toFixed(0)}% used{/${pctColor}-fg}` +
1252
- ` resets ${resetDay} ${resetHour}`;
1293
+ ` {${pctColor}-fg}⏱ ${countdown}{/${pctColor}-fg}` +
1294
+ ` resets ${resetTime}`;
1253
1295
  }
1254
1296
  windowBox.setContent(line1 + (line2 ? '\n' + line2 : ''));
1255
1297
  }
@@ -1262,6 +1304,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1262
1304
  }
1263
1305
  catch { }
1264
1306
  }
1307
+ // Legacy wrapper for backward compat
1308
+ async function updateWindowBox() { await fetchAndCacheUsage(); }
1265
1309
  // ── Handle terminal resize ──
1266
1310
  // Recalculate all widget positions from new screen.height
1267
1311
  screen.on('resize', () => {
@@ -1292,6 +1336,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1292
1336
  screen.key(['C-c'], () => {
1293
1337
  clearInterval(pollInterval);
1294
1338
  clearInterval(windowPollInterval);
1339
+ clearInterval(countdownInterval);
1295
1340
  clearInterval(headerAnimInterval);
1296
1341
  clearInterval(fortuneInterval);
1297
1342
  screen.destroy();
@@ -1301,6 +1346,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1301
1346
  screen.key(['q'], () => {
1302
1347
  clearInterval(pollInterval);
1303
1348
  clearInterval(windowPollInterval);
1349
+ clearInterval(countdownInterval);
1304
1350
  clearInterval(headerAnimInterval);
1305
1351
  clearInterval(fortuneInterval);
1306
1352
  screen.destroy();
@@ -1408,7 +1454,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1408
1454
  fortuneText = activeFortunes[fortuneIdx];
1409
1455
  renderHeader();
1410
1456
  }, 30000);
1411
- const windowPollInterval = setInterval(updateWindowBox, 15000); // every 15s
1457
+ const windowPollInterval = setInterval(fetchAndCacheUsage, 15000); // fetch fresh data every 15s
1458
+ const countdownInterval = setInterval(renderWindowBox, 1000); // tick countdown every 1s
1412
1459
  }
1413
1460
  // ── Helpers ──
1414
1461
  function fmtK(n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {