letmecode 0.1.25 → 0.1.28

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.
@@ -4,7 +4,7 @@ import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, rende
4
4
  import { buildHelpText, buildProviderStatsOptions, parseCliOptions } from "./cli-options.js";
5
5
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
6
6
  import { reportAnonymousUsage } from "./reporting.js";
7
- import { estimateLimitFullValue } from "./providers/limits.js";
7
+ import { estimateLimitFullValue, selectLatestActiveLimitWindows } from "./providers/limits.js";
8
8
  const ESC = String.fromCharCode(0x1b);
9
9
  // Normal mouse tracking (button press/release only) + SGR extended coordinates.
10
10
  // This makes the controls clickable while leaving Shift+drag native text selection
@@ -21,7 +21,8 @@ const DETAIL_VIEWS = [
21
21
  ];
22
22
  const CONTROL_SECTIONS = ["provider", "view", "table"];
23
23
  const CODEX_CREDIT_COST_USD = 0.01;
24
- const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq.", "Full"];
24
+ const ACTIVE_LIMIT_WINDOW_COLOR = "#FFA500";
25
+ const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Tokens", "Start", "End", "API eq.", "Full"];
25
26
  const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
26
27
  const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
27
28
  const COPILOT_ACTIONS = [
@@ -430,7 +431,7 @@ function buildTextTableLines(options) {
430
431
  key: row.key,
431
432
  text: buildTableRow(table, row.cells),
432
433
  inverse: isSelected,
433
- color: isSelected ? "cyan" : undefined
434
+ color: isSelected ? "cyan" : row.color
434
435
  }];
435
436
  if (options.separatorAfterRowKeys?.has(row.key)) {
436
437
  lines.push({
@@ -465,8 +466,10 @@ function buildTextTableLines(options) {
465
466
  };
466
467
  }
467
468
  function buildLimitWindowTableLines(stats, selectedRowKey) {
468
- const primaryRows = stats.primaryLimitWindows.map((window) => buildLimitWindowTableRow(window));
469
- const secondaryRows = stats.secondaryLimitWindows.map((window) => buildLimitWindowTableRow(window));
469
+ const allWindows = [...stats.primaryLimitWindows, ...stats.secondaryLimitWindows];
470
+ const activeWindows = selectLatestActiveLimitWindows(allWindows);
471
+ const primaryRows = stats.primaryLimitWindows.map((window) => buildLimitWindowTableRow(window, activeWindows.has(window)));
472
+ const secondaryRows = stats.secondaryLimitWindows.map((window) => buildLimitWindowTableRow(window, activeWindows.has(window)));
470
473
  const separatorAfterRowKeys = primaryRows.length > 0 && secondaryRows.length > 0
471
474
  ? new Set([primaryRows[primaryRows.length - 1].key])
472
475
  : undefined;
@@ -478,15 +481,17 @@ function buildLimitWindowTableLines(stats, selectedRowKey) {
478
481
  separatorAfterRowKeys
479
482
  });
480
483
  }
481
- function buildLimitWindowTableRow(window) {
484
+ function buildLimitWindowTableRow(window, isActive) {
482
485
  return {
483
486
  key: `limit-row:${getLimitRowKey(window)}`,
487
+ color: isActive ? ACTIVE_LIMIT_WINDOW_COLOR : undefined,
484
488
  cells: [
485
489
  window.scope,
486
490
  window.planType,
487
491
  formatLimitWindowModels(window),
488
492
  formatCompactWindowMinutes(window.windowMinutes),
489
493
  formatUsedPercentRange(window.minUsedPercent, window.maxUsedPercent),
494
+ formatCompactTokenCount(window.totals.totalTokens),
490
495
  formatCompactLocalDateTime(window.startTimeUtcIso),
491
496
  formatCompactLocalDateTime(window.endTimeUtcIso),
492
497
  // Status-aware: shows "-" when the API-equivalent cost is unknown rather
@@ -17,6 +17,7 @@ const RATE_CARD = {
17
17
  "claude-opus-4-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
18
18
  "claude-opus-4-1": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
19
19
  "claude-opus-4": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
20
+ "claude-opus-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
20
21
  "claude-sonnet-5": {
21
22
  input: 3,
22
23
  cacheRead: 0.3,
@@ -13,7 +13,44 @@ import { resolveUsageRate } from "./pricing.js";
13
13
  // provider. These are the real gpt-5.* API prices, not the (4x cheaper) Codex
14
14
  // subscription credit prices.
15
15
  const USD_TO_CREDITS = 100;
16
+ const GPT_5_6_SOL_RATE = {
17
+ input: 5,
18
+ cacheRead: 0.5,
19
+ cacheWrite: 6.25,
20
+ cacheWrite5m: 6.25,
21
+ cacheWrite1h: 6.25,
22
+ output: 30,
23
+ longContext: {
24
+ thresholdTokens: 272000,
25
+ rate: { input: 10, cacheRead: 1, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 12.5, output: 45 }
26
+ }
27
+ };
16
28
  const RATE_CARD = {
29
+ "gpt-5.6-sol": GPT_5_6_SOL_RATE,
30
+ "gpt-5.6-terra": {
31
+ input: 2.5,
32
+ cacheRead: 0.25,
33
+ cacheWrite: 3.125,
34
+ cacheWrite5m: 3.125,
35
+ cacheWrite1h: 3.125,
36
+ output: 15,
37
+ longContext: {
38
+ thresholdTokens: 272000,
39
+ rate: { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 6.25, output: 22.5 }
40
+ }
41
+ },
42
+ "gpt-5.6-luna": {
43
+ input: 1,
44
+ cacheRead: 0.1,
45
+ cacheWrite: 1.25,
46
+ cacheWrite5m: 1.25,
47
+ cacheWrite1h: 1.25,
48
+ output: 6,
49
+ longContext: {
50
+ thresholdTokens: 272000,
51
+ rate: { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 9 }
52
+ }
53
+ },
17
54
  "gpt-5.5": { input: 5, cacheRead: 0.5, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 30 },
18
55
  "gpt-5.4": { input: 2.5, cacheRead: 0.25, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 15 },
19
56
  "gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0.75, cacheWrite5m: 0.75, cacheWrite1h: 0.75, output: 4.5 }
@@ -53,7 +90,7 @@ export class CodexUsageProvider extends UsageProviderBase {
53
90
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
54
91
  const unknownPricedModels = modelUsage
55
92
  .map((row) => row.modelId)
56
- .filter((modelId) => !RATE_CARD[modelId] && !isAssumedZeroRatedCodexModel(modelId, knownModels));
93
+ .filter((modelId) => !rateForCodexModel(modelId) && !isAssumedZeroRatedCodexModel(modelId, knownModels));
57
94
  if (unknownPricedModels.length > 0) {
58
95
  warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
59
96
  }
@@ -235,7 +272,7 @@ function subtractRawUsage(current, previous) {
235
272
  };
236
273
  }
237
274
  function creditsFor(modelId, usage) {
238
- const rate = resolveUsageRate(RATE_CARD, modelId);
275
+ const rate = rateForCodexModel(modelId, usage.inputTokens);
239
276
  if (!rate) {
240
277
  return 0;
241
278
  }
@@ -246,6 +283,12 @@ function creditsFor(modelId, usage) {
246
283
  (usage.outputTokens / 1000000) * rate.output) *
247
284
  USD_TO_CREDITS);
248
285
  }
286
+ function rateForCodexModel(modelId, inputTokens = 0) {
287
+ // The unsuffixed API alias routes to Sol. Normalize only the exact alias so
288
+ // an unknown future gpt-5.6-* tier is not accidentally charged at Sol rates.
289
+ const pricedModelId = modelId === "gpt-5.6" ? "gpt-5.6-sol" : modelId;
290
+ return resolveUsageRate(RATE_CARD, pricedModelId, inputTokens, { prefixMatch: true });
291
+ }
249
292
  function rawUsageToTotals(usage) {
250
293
  const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
251
294
  const inputTokens = Math.max(0, usage.inputTokens - cacheReadInputTokens);
@@ -267,7 +310,7 @@ function createUsageTotalsForModel(modelId, usage, knownModels) {
267
310
  const deltaTotals = rawUsageToTotals(usage);
268
311
  deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
269
312
  deltaTotals.eventCount = 1;
270
- if (!RATE_CARD[resolvedModelId] && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
313
+ if (!rateForCodexModel(resolvedModelId, usage.inputTokens) && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
271
314
  deltaTotals.estimatedCreditsStatus = "unavailable";
272
315
  }
273
316
  return deltaTotals;
@@ -19,6 +19,7 @@ export const RATE_CARD = {
19
19
  "claude-opus-4-6": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
20
20
  "claude-opus-4-7": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
21
21
  "claude-opus-4-8": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
22
+ "claude-opus-5": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
22
23
  "claude-fable-5": { input: 1000, cacheRead: 100, cacheWrite: 1250, cacheWrite5m: 1250, cacheWrite1h: 2000, output: 5000 },
23
24
  "gemini-2.5-pro": { input: 125, cacheRead: 12.5, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 125, output: 1000 },
24
25
  "gemini-3-flash": { input: 50, cacheRead: 5, cacheWrite: 50, cacheWrite5m: 50, cacheWrite1h: 50, output: 300 },
@@ -1,7 +1,32 @@
1
1
  import { addUsageTotals, cloneUsageTotals, createEmptyUsageTotals } from "./contract.js";
2
+ // Recent Codex monthly windows can report a reset timestamp that jitters by a
3
+ // few minutes while the logical quota cycle remains unchanged. Short and weekly
4
+ // windows retain exact reset identity so their established behavior is stable.
5
+ const MONTHLY_WINDOW_MINUTES = 30 * 24 * 60;
6
+ const MONTHLY_RESET_JITTER_SECONDS = 15 * 60;
2
7
  export function createLimitWindowAggregates() {
3
8
  return new Map();
4
9
  }
10
+ export function isLimitWindowActive(window, nowMs = Date.now()) {
11
+ const startMs = Date.parse(window.startTimeUtcIso);
12
+ const endMs = Date.parse(window.endTimeUtcIso);
13
+ return Number.isFinite(startMs) && Number.isFinite(endMs) && startMs <= nowMs && nowMs <= endMs;
14
+ }
15
+ export function selectLatestActiveLimitWindows(windows, nowMs = Date.now()) {
16
+ const latestByPlanAndWindow = new Map();
17
+ for (const window of windows) {
18
+ if (!isLimitWindowActive(window, nowMs)) {
19
+ continue;
20
+ }
21
+ const startMs = Date.parse(window.startTimeUtcIso);
22
+ const groupKey = JSON.stringify([window.planType, window.windowMinutes]);
23
+ const current = latestByPlanAndWindow.get(groupKey);
24
+ if (!current || startMs > current.startMs) {
25
+ latestByPlanAndWindow.set(groupKey, { window, startMs });
26
+ }
27
+ }
28
+ return new Set([...latestByPlanAndWindow.values()].map((entry) => entry.window));
29
+ }
5
30
  export function numberOrZero(value) {
6
31
  if (typeof value === "number") {
7
32
  return Number.isFinite(value) ? value : 0;
@@ -92,6 +117,35 @@ function makeWindowKey(scope, rateLimits, window) {
92
117
  numberOrZero(window.resets_at)
93
118
  ].join("|");
94
119
  }
120
+ function findMatchingWindowKey(windows, scope, rateLimits, windowMinutes, resetsAt) {
121
+ const exactKey = makeWindowKey(scope, rateLimits, { window_minutes: windowMinutes, resets_at: resetsAt });
122
+ if (windows.has(exactKey) || windowMinutes < MONTHLY_WINDOW_MINUTES) {
123
+ return exactKey;
124
+ }
125
+ const limitId = String(rateLimits.limit_id ?? "unknown");
126
+ const planType = String(rateLimits.plan_type ?? "unknown");
127
+ let closestKey = exactKey;
128
+ let closestDistance = Number.POSITIVE_INFINITY;
129
+ for (const [key, candidate] of windows) {
130
+ if (candidate.scope !== scope ||
131
+ candidate.limitId !== limitId ||
132
+ candidate.planType !== planType ||
133
+ candidate.windowMinutes !== windowMinutes) {
134
+ continue;
135
+ }
136
+ const minResetsAt = candidate.minStartsAt + candidate.windowMinutes * 60;
137
+ const distance = resetsAt < minResetsAt
138
+ ? minResetsAt - resetsAt
139
+ : resetsAt > candidate.maxResetsAt
140
+ ? resetsAt - candidate.maxResetsAt
141
+ : 0;
142
+ if (distance <= MONTHLY_RESET_JITTER_SECONDS && distance < closestDistance) {
143
+ closestKey = key;
144
+ closestDistance = distance;
145
+ }
146
+ }
147
+ return closestKey;
148
+ }
95
149
  function collapseNearbyWindows(rows) {
96
150
  const collapsed = new Map();
97
151
  for (const row of rows) {
@@ -163,7 +217,7 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, modelId,
163
217
  }
164
218
  const startsAt = resetsAt - windowMinutes * 60;
165
219
  const usedPercent = numberOrZero(window.used_percent);
166
- const key = makeWindowKey(scope, rateLimits, window);
220
+ const key = findMatchingWindowKey(windows, scope, rateLimits, windowMinutes, resetsAt);
167
221
  const existing = windows.get(key);
168
222
  if (!existing) {
169
223
  windows.set(key, {
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.25",
3
+ "version": "0.1.28",
4
4
  "description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
5
5
  "author": "Devforth (https://devforth.io)",
6
6
  "license": "MIT",
7
+ "packageManager": "pnpm@10.28.2",
7
8
  "type": "commonjs",
8
9
  "main": "./dist/index.js",
9
10
  "bin": {
@@ -29,6 +30,16 @@
29
30
  "publishConfig": {
30
31
  "access": "public"
31
32
  },
33
+ "scripts": {
34
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
35
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
36
+ "prepack": "npm run build",
37
+ "prestart": "npm run build",
38
+ "start": "node ./bin/letmecode.js",
39
+ "pretest": "npm run build",
40
+ "smoke": "node ./bin/letmecode.js",
41
+ "test": "node --test ink-app/test/*.test.mjs"
42
+ },
32
43
  "keywords": [
33
44
  "ai",
34
45
  "agents",
@@ -53,14 +64,5 @@
53
64
  "@types/node": "^24.0.7",
54
65
  "@types/react": "^18.3.24",
55
66
  "typescript": "^5.8.3"
56
- },
57
- "scripts": {
58
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
59
- "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
60
- "prestart": "npm run build",
61
- "start": "node ./bin/letmecode.js",
62
- "pretest": "npm run build",
63
- "smoke": "node ./bin/letmecode.js",
64
- "test": "node --test ink-app/test/*.test.mjs"
65
67
  }
66
- }
68
+ }