letmecode 0.1.7 → 0.1.9
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.
|
@@ -96,7 +96,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
96
96
|
const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
|
|
97
97
|
const safeEventTimeMs = Number.isFinite(event.timestampMs) ? event.timestampMs : 0;
|
|
98
98
|
addDailyUsage(byDay, event.timestampMs, event.modelId, planType, event.totals);
|
|
99
|
-
applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.totals, planTypes);
|
|
99
|
+
applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.modelId, event.totals, planTypes);
|
|
100
100
|
}
|
|
101
101
|
parseTotals.tokenEvents = selectedEvents.length;
|
|
102
102
|
if (parseTotals.malformedLines > 0) {
|
|
@@ -493,6 +493,12 @@ function describeUsageOutput(output) {
|
|
|
493
493
|
}
|
|
494
494
|
return output.trim() ? output : "<empty>";
|
|
495
495
|
}
|
|
496
|
+
function buildClaudeCommandEnvironment() {
|
|
497
|
+
return {
|
|
498
|
+
...process.env,
|
|
499
|
+
TZ: "UTC"
|
|
500
|
+
};
|
|
501
|
+
}
|
|
496
502
|
async function buildLiveLimitWindows(options) {
|
|
497
503
|
const [usageOutput, subscriptionType] = await Promise.all([
|
|
498
504
|
readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput, options.traceLogger),
|
|
@@ -541,9 +547,10 @@ async function readClaudeAuthStatusOutput(root, usageCommandKind, override, trac
|
|
|
541
547
|
return null;
|
|
542
548
|
}
|
|
543
549
|
try {
|
|
544
|
-
traceClaude(traceLogger, usageCommandKind, `Running auth status command with ${binaryPath}.`);
|
|
550
|
+
traceClaude(traceLogger, usageCommandKind, `Running auth status command with ${binaryPath} (TZ=UTC).`);
|
|
545
551
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
|
|
546
552
|
encoding: "utf8",
|
|
553
|
+
env: buildClaudeCommandEnvironment(),
|
|
547
554
|
maxBuffer: 1024 * 1024,
|
|
548
555
|
timeout: 15000,
|
|
549
556
|
windowsHide: true
|
|
@@ -626,9 +633,10 @@ async function readClaudeUsageCommandOutput(root, usageCommandKind, override, tr
|
|
|
626
633
|
return null;
|
|
627
634
|
}
|
|
628
635
|
try {
|
|
629
|
-
traceClaude(traceLogger, usageCommandKind, `Running /usage command with ${binaryPath}.`);
|
|
636
|
+
traceClaude(traceLogger, usageCommandKind, `Running /usage command with ${binaryPath} (TZ=UTC).`);
|
|
630
637
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
|
|
631
638
|
encoding: "utf8",
|
|
639
|
+
env: buildClaudeCommandEnvironment(),
|
|
632
640
|
maxBuffer: 1024 * 1024,
|
|
633
641
|
timeout: 15000,
|
|
634
642
|
windowsHide: true
|
|
@@ -864,9 +872,19 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
864
872
|
minUsedPercent: snapshot.usedPercent,
|
|
865
873
|
maxUsedPercent: snapshot.usedPercent,
|
|
866
874
|
totals,
|
|
875
|
+
modelUsage: buildModelUsageRowsForEvents(inWindowEvents),
|
|
867
876
|
eventCount: totals.eventCount
|
|
868
877
|
};
|
|
869
878
|
}
|
|
879
|
+
function buildModelUsageRowsForEvents(events) {
|
|
880
|
+
const byModel = new Map();
|
|
881
|
+
for (const event of events) {
|
|
882
|
+
addModelUsage(byModel, event.modelId, event.totals);
|
|
883
|
+
}
|
|
884
|
+
return [...byModel.entries()]
|
|
885
|
+
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
886
|
+
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
887
|
+
}
|
|
870
888
|
function toUtcIso(value) {
|
|
871
889
|
return new Date(value).toISOString().replace(".000Z", "Z");
|
|
872
890
|
}
|
|
@@ -349,7 +349,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
|
|
|
349
349
|
const rateLimits = asRecord(payload.rate_limits);
|
|
350
350
|
const planType = typeof rateLimits?.plan_type === "string" ? rateLimits.plan_type : undefined;
|
|
351
351
|
addDailyUsage(byDay, eventTimeMs, resolvedModelId, planType, deltaTotals);
|
|
352
|
-
applyRateLimits(windows, rateLimits, safeEventTimeMs, deltaTotals, planTypes);
|
|
352
|
+
applyRateLimits(windows, rateLimits, safeEventTimeMs, resolvedModelId, deltaTotals, planTypes);
|
|
353
353
|
}
|
|
354
354
|
return { linesRead, tokenEvents, malformedLines };
|
|
355
355
|
}
|
|
@@ -8,31 +8,35 @@ export function numberOrZero(value) {
|
|
|
8
8
|
export function asRecord(value) {
|
|
9
9
|
return value && typeof value === "object" ? value : null;
|
|
10
10
|
}
|
|
11
|
-
export function applyRateLimits(windows, rateLimits, eventTimeMs, deltaTotals, planTypes) {
|
|
11
|
+
export function applyRateLimits(windows, rateLimits, eventTimeMs, modelId, deltaTotals, planTypes) {
|
|
12
12
|
if (!rateLimits) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (typeof rateLimits.plan_type === "string") {
|
|
16
16
|
planTypes.add(rateLimits.plan_type);
|
|
17
17
|
}
|
|
18
|
-
upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, deltaTotals);
|
|
19
|
-
upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, deltaTotals);
|
|
18
|
+
upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, modelId, deltaTotals);
|
|
19
|
+
upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, modelId, deltaTotals);
|
|
20
20
|
}
|
|
21
21
|
export function buildWindowLists(windows) {
|
|
22
|
-
const rows = collapseNearbyWindows([...windows.values()].map((window) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
const rows = collapseNearbyWindows([...windows.values()].map((window) => {
|
|
23
|
+
const usage = computeWindowUsage(window.events);
|
|
24
|
+
return {
|
|
25
|
+
scope: window.scope,
|
|
26
|
+
planType: window.planType,
|
|
27
|
+
limitId: window.limitId,
|
|
28
|
+
windowMinutes: window.windowMinutes,
|
|
29
|
+
startTimeUtcIso: formatIsoFromSeconds(window.minStartsAt),
|
|
30
|
+
endTimeUtcIso: formatIsoFromSeconds(window.maxResetsAt),
|
|
31
|
+
firstSeenUtcIso: formatIsoFromMilliseconds(window.firstSeenMs),
|
|
32
|
+
lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
|
|
33
|
+
minUsedPercent: window.minUsedPercent,
|
|
34
|
+
maxUsedPercent: window.maxUsedPercent,
|
|
35
|
+
totals: usage.totals,
|
|
36
|
+
modelUsage: usage.modelUsage,
|
|
37
|
+
eventCount: 0
|
|
38
|
+
};
|
|
39
|
+
}))
|
|
36
40
|
.map((row) => ({
|
|
37
41
|
...row,
|
|
38
42
|
eventCount: row.totals.eventCount
|
|
@@ -71,7 +75,11 @@ function collapseNearbyWindows(rows) {
|
|
|
71
75
|
if (!existing) {
|
|
72
76
|
collapsed.set(key, {
|
|
73
77
|
...row,
|
|
74
|
-
totals: cloneUsageTotals(row.totals)
|
|
78
|
+
totals: cloneUsageTotals(row.totals),
|
|
79
|
+
modelUsage: row.modelUsage.map((entry) => ({
|
|
80
|
+
modelId: entry.modelId,
|
|
81
|
+
totals: cloneUsageTotals(entry.totals)
|
|
82
|
+
}))
|
|
75
83
|
});
|
|
76
84
|
continue;
|
|
77
85
|
}
|
|
@@ -86,28 +94,34 @@ function collapseNearbyWindows(rows) {
|
|
|
86
94
|
existing.minUsedPercent = Math.min(existing.minUsedPercent, row.minUsedPercent);
|
|
87
95
|
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, row.maxUsedPercent);
|
|
88
96
|
addUsageTotals(existing.totals, row.totals);
|
|
97
|
+
existing.modelUsage = mergeModelUsageRows(existing.modelUsage, row.modelUsage);
|
|
89
98
|
existing.eventCount = existing.totals.eventCount;
|
|
90
99
|
}
|
|
91
100
|
return [...collapsed.values()];
|
|
92
101
|
}
|
|
93
|
-
function
|
|
102
|
+
function computeWindowUsage(events) {
|
|
94
103
|
// Session files are not guaranteed to be parsed in timestamp order, so
|
|
95
104
|
// saturation has to be applied after we sort the captured window events.
|
|
96
105
|
const totals = createEmptyUsageTotals();
|
|
106
|
+
const byModel = new Map();
|
|
97
107
|
let sawBelowCap = false;
|
|
98
108
|
let isExhausted = false;
|
|
99
109
|
for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
|
|
100
110
|
sawBelowCap || (sawBelowCap = event.usedPercent < 100);
|
|
101
111
|
if (!isExhausted) {
|
|
102
112
|
addUsageTotals(totals, event.totals);
|
|
113
|
+
addWindowModelUsage(byModel, event.modelId, event.totals);
|
|
103
114
|
if (sawBelowCap && event.usedPercent >= 100) {
|
|
104
115
|
isExhausted = true;
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
|
-
return
|
|
119
|
+
return {
|
|
120
|
+
totals,
|
|
121
|
+
modelUsage: buildModelUsageRows(byModel)
|
|
122
|
+
};
|
|
109
123
|
}
|
|
110
|
-
function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTotals) {
|
|
124
|
+
function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, modelId, deltaTotals) {
|
|
111
125
|
if (!window) {
|
|
112
126
|
return;
|
|
113
127
|
}
|
|
@@ -132,7 +146,7 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
|
|
|
132
146
|
lastSeenMs: eventTimeMs,
|
|
133
147
|
minUsedPercent: usedPercent,
|
|
134
148
|
maxUsedPercent: usedPercent,
|
|
135
|
-
events: [{ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
|
|
149
|
+
events: [{ eventTimeMs, modelId, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
|
|
136
150
|
});
|
|
137
151
|
return;
|
|
138
152
|
}
|
|
@@ -142,5 +156,25 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
|
|
|
142
156
|
existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
|
|
143
157
|
existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
|
|
144
158
|
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
|
|
145
|
-
existing.events.push({ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) });
|
|
159
|
+
existing.events.push({ eventTimeMs, modelId, usedPercent, totals: cloneUsageTotals(deltaTotals) });
|
|
160
|
+
}
|
|
161
|
+
function addWindowModelUsage(byModel, modelId, totals) {
|
|
162
|
+
const existing = byModel.get(modelId);
|
|
163
|
+
if (!existing) {
|
|
164
|
+
byModel.set(modelId, cloneUsageTotals(totals));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
addUsageTotals(existing, totals);
|
|
168
|
+
}
|
|
169
|
+
function buildModelUsageRows(byModel) {
|
|
170
|
+
return [...byModel.entries()]
|
|
171
|
+
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
172
|
+
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
173
|
+
}
|
|
174
|
+
function mergeModelUsageRows(left, right) {
|
|
175
|
+
const byModel = new Map();
|
|
176
|
+
for (const row of [...left, ...right]) {
|
|
177
|
+
addWindowModelUsage(byModel, row.modelId, row.totals);
|
|
178
|
+
}
|
|
179
|
+
return buildModelUsageRows(byModel);
|
|
146
180
|
}
|
|
@@ -37,25 +37,23 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
|
|
|
37
37
|
used_percents: resolveReportedUsedPercents(window),
|
|
38
38
|
used_exhausted: window.maxUsedPercent >= 100,
|
|
39
39
|
value_dollars: roundDollars(window.totals.estimatedCredits * CREDIT_TO_DOLLARS),
|
|
40
|
-
usage_raw: buildUsageRaw(
|
|
40
|
+
usage_raw: buildUsageRaw(window.modelUsage),
|
|
41
41
|
letmecode_version: letmecodeVersion
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
function buildUsageRaw(
|
|
45
|
-
const usageRaw = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
function buildUsageRaw(modelUsage) {
|
|
45
|
+
const usageRaw = {};
|
|
46
|
+
for (const row of modelUsage) {
|
|
47
|
+
usageRaw[row.modelId] = {
|
|
48
|
+
output: row.totals.outputTokens,
|
|
49
|
+
input_non_cache: row.totals.inputTokens,
|
|
50
|
+
input_cache_w5m: row.totals.cacheWrite5mInputTokens,
|
|
51
|
+
input_cache_w1h: row.totals.cacheWrite1hInputTokens,
|
|
52
|
+
input_cache_read: row.totals.cacheReadInputTokens
|
|
53
|
+
};
|
|
53
54
|
}
|
|
54
55
|
return usageRaw;
|
|
55
56
|
}
|
|
56
|
-
function isAnthropicProvider(providerId) {
|
|
57
|
-
return providerId === "claude" || providerId === "claude-vscode";
|
|
58
|
-
}
|
|
59
57
|
function resolveReportedUsedPercents(window) {
|
|
60
58
|
if (window.minUsedPercent === window.maxUsedPercent) {
|
|
61
59
|
return clampPercent(window.maxUsedPercent);
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Provider-based terminal usage dashboard for LetMeCode.",
|
|
5
5
|
"author": "devforth.io",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"packageManager": "pnpm@10.28.2",
|
|
7
8
|
"type": "commonjs",
|
|
8
9
|
"bin": {
|
|
9
10
|
"letmecode": "./bin/letmecode.js"
|
|
@@ -20,6 +21,16 @@
|
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public"
|
|
22
23
|
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
26
|
+
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
27
|
+
"prepack": "npm run build",
|
|
28
|
+
"prestart": "npm run build",
|
|
29
|
+
"start": "node ./bin/letmecode.js",
|
|
30
|
+
"pretest": "npm run build",
|
|
31
|
+
"smoke": "node ./bin/letmecode.js",
|
|
32
|
+
"test": "node --test ink-app/test/*.test.mjs"
|
|
33
|
+
},
|
|
23
34
|
"keywords": [
|
|
24
35
|
"cli",
|
|
25
36
|
"ink",
|
|
@@ -36,14 +47,5 @@
|
|
|
36
47
|
"@types/node": "^24.0.7",
|
|
37
48
|
"@types/react": "^18.3.24",
|
|
38
49
|
"typescript": "^5.8.3"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
42
|
-
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
43
|
-
"prestart": "npm run build",
|
|
44
|
-
"start": "node ./bin/letmecode.js",
|
|
45
|
-
"pretest": "npm run build",
|
|
46
|
-
"smoke": "node ./bin/letmecode.js",
|
|
47
|
-
"test": "node --test ink-app/test/*.test.mjs"
|
|
48
50
|
}
|
|
49
|
-
}
|
|
51
|
+
}
|