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.
package/ink-app/dist/index.js
CHANGED
|
@@ -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
|
|
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" :
|
|
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
|
|
469
|
-
const
|
|
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) => !
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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.
|
|
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
|
+
}
|