letmecode 0.1.21 → 0.1.22
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.
|
@@ -10,12 +10,26 @@ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregate
|
|
|
10
10
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
11
11
|
import { resolveUsageRate } from "./pricing.js";
|
|
12
12
|
const RATE_CARD = {
|
|
13
|
+
"claude-fable-5": { input: 10, cacheRead: 1, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 20, output: 50 },
|
|
13
14
|
"claude-opus-4-8": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
14
15
|
"claude-opus-4-7": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
15
16
|
"claude-opus-4-6": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
16
17
|
"claude-opus-4-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
17
18
|
"claude-opus-4-1": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
18
19
|
"claude-opus-4": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
20
|
+
"claude-sonnet-5": {
|
|
21
|
+
input: 3,
|
|
22
|
+
cacheRead: 0.3,
|
|
23
|
+
cacheWrite: 3.75,
|
|
24
|
+
cacheWrite5m: 3.75,
|
|
25
|
+
cacheWrite1h: 6,
|
|
26
|
+
output: 15,
|
|
27
|
+
// Intro pricing (~0.66x) through 2026-08-31; reverts to the standard rate above on 2026-09-01.
|
|
28
|
+
introOffer: {
|
|
29
|
+
effectiveUntilMs: Date.UTC(2026, 8, 1),
|
|
30
|
+
rate: { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
19
33
|
"claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
20
34
|
"claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
21
35
|
"claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
@@ -46,12 +60,14 @@ const parsedClaudeSessionFilesCache = new Map();
|
|
|
46
60
|
const claudeBinaryPathCache = new Map();
|
|
47
61
|
const claudeUsageOutputCache = new Map();
|
|
48
62
|
const claudeAuthStatusOutputCache = new Map();
|
|
63
|
+
const claudeOauthCredentialsOutputCache = new Map();
|
|
49
64
|
export class ClaudeUsageProvider extends UsageProviderBase {
|
|
50
65
|
constructor(options = {}) {
|
|
51
66
|
super(options.id ?? "claude", options.label ?? "Claude");
|
|
52
67
|
this.root = path.resolve(options.root ?? os.homedir());
|
|
53
68
|
this.readUsageCommandOutput = options.readUsageCommandOutput;
|
|
54
69
|
this.readAuthStatusOutput = options.readAuthStatusOutput;
|
|
70
|
+
this.readOauthCredentials = options.readOauthCredentials;
|
|
55
71
|
this.now = options.now ?? (() => new Date());
|
|
56
72
|
}
|
|
57
73
|
async getStats(options = {}) {
|
|
@@ -147,7 +163,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
147
163
|
const liveLimitWindows = await buildLiveLimitWindows({
|
|
148
164
|
root: this.root,
|
|
149
165
|
readUsageCommandOutput: this.readUsageCommandOutput,
|
|
150
|
-
|
|
166
|
+
readOauthCredentials: this.readOauthCredentials,
|
|
151
167
|
traceLogger: options.traceLogger,
|
|
152
168
|
now: this.now(),
|
|
153
169
|
selectedEvents
|
|
@@ -212,14 +228,14 @@ function normalizeUsage(value) {
|
|
|
212
228
|
inferenceGeo: String(usage.inference_geo ?? "")
|
|
213
229
|
};
|
|
214
230
|
}
|
|
215
|
-
function resolveRate(modelId) {
|
|
216
|
-
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true });
|
|
231
|
+
function resolveRate(modelId, timestampMs) {
|
|
232
|
+
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true, timestampMs });
|
|
217
233
|
}
|
|
218
234
|
function isInternalClaudeModel(modelId) {
|
|
219
235
|
return modelId === "<synthetic>";
|
|
220
236
|
}
|
|
221
|
-
function creditsFor(modelId, usage) {
|
|
222
|
-
const rate = resolveRate(modelId);
|
|
237
|
+
function creditsFor(modelId, usage, timestampMs) {
|
|
238
|
+
const rate = resolveRate(modelId, timestampMs);
|
|
223
239
|
if (!rate) {
|
|
224
240
|
return 0;
|
|
225
241
|
}
|
|
@@ -233,7 +249,7 @@ function creditsFor(modelId, usage) {
|
|
|
233
249
|
inferenceMultiplier *
|
|
234
250
|
USD_TO_CREDITS);
|
|
235
251
|
}
|
|
236
|
-
function usageToTotals(modelId, usage) {
|
|
252
|
+
function usageToTotals(modelId, usage, timestampMs) {
|
|
237
253
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
238
254
|
const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
239
255
|
cacheWriteBreakdown.cacheWrite1hInputTokens;
|
|
@@ -249,7 +265,7 @@ function usageToTotals(modelId, usage) {
|
|
|
249
265
|
usage.cacheReadInputTokens +
|
|
250
266
|
cacheWriteInputTokens +
|
|
251
267
|
usage.outputTokens,
|
|
252
|
-
estimatedCredits: creditsFor(modelId, usage),
|
|
268
|
+
estimatedCredits: creditsFor(modelId, usage, timestampMs),
|
|
253
269
|
eventCount: 1
|
|
254
270
|
};
|
|
255
271
|
}
|
|
@@ -437,7 +453,7 @@ async function parseSessionFile(filePath) {
|
|
|
437
453
|
timestampMs: eventTimeMs,
|
|
438
454
|
modelId,
|
|
439
455
|
usage: normalizedUsage,
|
|
440
|
-
totals: usageToTotals(modelId, normalizedUsage),
|
|
456
|
+
totals: usageToTotals(modelId, normalizedUsage, eventTimeMs),
|
|
441
457
|
rateLimits
|
|
442
458
|
});
|
|
443
459
|
}
|
|
@@ -546,7 +562,7 @@ function mergeParsedUsageEvents(previous, next) {
|
|
|
546
562
|
timestampMs: Math.max(normalizeTimestamp(previous.timestampMs), normalizeTimestamp(next.timestampMs)),
|
|
547
563
|
modelId,
|
|
548
564
|
usage,
|
|
549
|
-
totals: usageToTotals(modelId, usage),
|
|
565
|
+
totals: usageToTotals(modelId, usage, primaryEvent.timestampMs),
|
|
550
566
|
rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
|
|
551
567
|
};
|
|
552
568
|
}
|
|
@@ -648,16 +664,16 @@ function buildClaudeCommandEnvironment() {
|
|
|
648
664
|
};
|
|
649
665
|
}
|
|
650
666
|
async function buildLiveLimitWindows(options) {
|
|
651
|
-
const [usageOutput,
|
|
667
|
+
const [usageOutput, credentials] = await Promise.all([
|
|
652
668
|
readClaudeUsageCommandOutput(options.root, options.readUsageCommandOutput, options.traceLogger),
|
|
653
|
-
|
|
669
|
+
readClaudeOauthCredentials(options.root, options.readOauthCredentials, options.traceLogger)
|
|
654
670
|
]);
|
|
655
671
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
656
672
|
traceClaude(options.traceLogger, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
657
673
|
if (snapshots.length === 0) {
|
|
658
674
|
traceClaude(options.traceLogger, "No live usage snapshots matched the expected /usage format.");
|
|
659
675
|
}
|
|
660
|
-
const resolvedPlanType = resolveClaudeLivePlanType(
|
|
676
|
+
const resolvedPlanType = resolveClaudeLivePlanType(credentials);
|
|
661
677
|
traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
|
|
662
678
|
const primaryLimitWindows = snapshots
|
|
663
679
|
.filter((snapshot) => snapshot.scope === "primary")
|
|
@@ -690,20 +706,82 @@ async function buildLiveLimitWindows(options) {
|
|
|
690
706
|
secondaryLimitWindows
|
|
691
707
|
};
|
|
692
708
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
709
|
+
// Reported as-is: the claude.ai OAuth `rateLimitTier` is not mapped to a friendly tier name
|
|
710
|
+
// anymore, it is combined with `subscriptionType` verbatim (e.g. "team|default_claude_max_5x").
|
|
711
|
+
function resolveClaudeLivePlanType(credentials) {
|
|
712
|
+
const subscriptionType = credentials?.subscriptionType;
|
|
713
|
+
if (!subscriptionType) {
|
|
714
|
+
return "live";
|
|
715
|
+
}
|
|
716
|
+
return credentials?.rateLimitTier ? `${subscriptionType}|${credentials.rateLimitTier}` : subscriptionType;
|
|
717
|
+
}
|
|
718
|
+
async function readClaudeOauthCredentials(root, override, traceLogger) {
|
|
719
|
+
const output = await readClaudeOauthCredentialsOutput(root, override, traceLogger);
|
|
720
|
+
const credentials = parseClaudeOauthCredentials(output);
|
|
721
|
+
if (output && !credentials) {
|
|
722
|
+
traceClaude(traceLogger, "Could not parse claude.ai OAuth credentials from credentials file.");
|
|
723
|
+
}
|
|
724
|
+
traceClaude(traceLogger, `OAuth credentials result: subscriptionType=${credentials?.subscriptionType ?? "<none>"} rateLimitTier=${credentials?.rateLimitTier ?? "<none>"}.`);
|
|
725
|
+
return credentials;
|
|
726
|
+
}
|
|
727
|
+
async function readClaudeOauthCredentialsOutput(root, override, traceLogger) {
|
|
728
|
+
if (override) {
|
|
729
|
+
try {
|
|
730
|
+
const output = await override();
|
|
731
|
+
traceClaude(traceLogger, "Using injected OAuth credentials override.");
|
|
732
|
+
return output;
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
traceClaude(traceLogger, "Injected OAuth credentials override failed.");
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const credentialsPath = path.join(path.resolve(root), ".claude", ".credentials.json");
|
|
740
|
+
const cacheKey = credentialsPath;
|
|
741
|
+
const cached = claudeOauthCredentialsOutputCache.get(cacheKey);
|
|
742
|
+
if (cached) {
|
|
743
|
+
traceClaude(traceLogger, "OAuth credentials cache hit.");
|
|
744
|
+
return cached;
|
|
696
745
|
}
|
|
697
|
-
|
|
746
|
+
const pending = (async () => {
|
|
747
|
+
try {
|
|
748
|
+
const output = await fs.promises.readFile(credentialsPath, { encoding: "utf8" });
|
|
749
|
+
traceClaude(traceLogger, `Read OAuth credentials from ${credentialsPath}.`);
|
|
750
|
+
return output;
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
traceClaude(traceLogger, `No readable OAuth credentials at ${credentialsPath}.`);
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
})();
|
|
757
|
+
claudeOauthCredentialsOutputCache.set(cacheKey, pending);
|
|
758
|
+
return pending;
|
|
698
759
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
760
|
+
function parseClaudeOauthCredentials(output) {
|
|
761
|
+
if (!output) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
let payload;
|
|
765
|
+
try {
|
|
766
|
+
payload = JSON.parse(output);
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const oauth = asRecord(asRecord(payload)?.claudeAiOauth);
|
|
772
|
+
if (!oauth) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const subscriptionType = typeof oauth.subscriptionType === "string" && oauth.subscriptionType.trim()
|
|
776
|
+
? oauth.subscriptionType.trim()
|
|
777
|
+
: null;
|
|
778
|
+
const rateLimitTier = typeof oauth.rateLimitTier === "string" && oauth.rateLimitTier.trim()
|
|
779
|
+
? oauth.rateLimitTier.trim()
|
|
780
|
+
: null;
|
|
781
|
+
if (!subscriptionType && !rateLimitTier) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
return { subscriptionType, rateLimitTier };
|
|
707
785
|
}
|
|
708
786
|
async function readClaudeAuthStatusOutput(root, override, traceLogger) {
|
|
709
787
|
if (override) {
|
|
@@ -751,10 +829,6 @@ async function readClaudeAuthStatusOutput(root, override, traceLogger) {
|
|
|
751
829
|
claudeAuthStatusOutputCache.set(cacheKey, pending);
|
|
752
830
|
return pending;
|
|
753
831
|
}
|
|
754
|
-
function parseClaudeSubscriptionType(output) {
|
|
755
|
-
const snapshot = parseClaudeAuthStatusSnapshot(output);
|
|
756
|
-
return snapshot?.subscriptionType ?? null;
|
|
757
|
-
}
|
|
758
832
|
function parseClaudeAuthStatusSnapshot(output) {
|
|
759
833
|
if (!output) {
|
|
760
834
|
return null;
|
|
@@ -977,9 +1051,6 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
977
1051
|
resetString: match[4]?.trim() || null
|
|
978
1052
|
});
|
|
979
1053
|
}
|
|
980
|
-
// Per-scope reset times printed by Claude. Lines like "Current week (Sonnet
|
|
981
|
-
// only)" omit the reset because it is identical to the "all models" week, so
|
|
982
|
-
// a missing reset inherits the resolved reset of the same scope.
|
|
983
1054
|
const resetMsByLabel = new Map();
|
|
984
1055
|
for (const parsed of parsedLines) {
|
|
985
1056
|
if (!parsed.resetString || resetMsByLabel.has(parsed.label)) {
|
|
@@ -996,14 +1067,17 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
996
1067
|
if (!resetsAtMs) {
|
|
997
1068
|
continue;
|
|
998
1069
|
}
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1070
|
+
const isModelScopedWeek = parsed.label === "week" && parsed.windowQualifier !== "" && parsed.windowQualifier !== "all models";
|
|
1071
|
+
const modelFamily = isModelScopedWeek ? parsed.windowQualifier.replace(/\s+only$/, "") : null;
|
|
1072
|
+
const limitId = modelFamily
|
|
1073
|
+
? `current-week-${modelFamily.replace(/\s+/g, "-")}-only`
|
|
1074
|
+
: `current-${parsed.label}`;
|
|
1001
1075
|
snapshots.set(limitId, {
|
|
1002
1076
|
scope: parsed.label === "session" ? "primary" : "secondary",
|
|
1003
1077
|
label: parsed.label,
|
|
1004
1078
|
limitId,
|
|
1005
|
-
|
|
1006
|
-
modelType:
|
|
1079
|
+
modelFamily,
|
|
1080
|
+
modelType: modelFamily ? parsed.windowQualifier : undefined,
|
|
1007
1081
|
usedPercent: parsed.usedPercent,
|
|
1008
1082
|
resetsAtMs,
|
|
1009
1083
|
windowMinutes: parsed.windowMinutes
|
|
@@ -1087,10 +1161,10 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1087
1161
|
};
|
|
1088
1162
|
}
|
|
1089
1163
|
function matchesClaudeLiveSnapshotModelScope(snapshot, modelId) {
|
|
1090
|
-
if (snapshot.
|
|
1164
|
+
if (!snapshot.modelFamily) {
|
|
1091
1165
|
return true;
|
|
1092
1166
|
}
|
|
1093
|
-
return modelId.toLowerCase().includes(
|
|
1167
|
+
return modelId.toLowerCase().includes(snapshot.modelFamily);
|
|
1094
1168
|
}
|
|
1095
1169
|
function buildModelUsageRowsForEvents(events) {
|
|
1096
1170
|
const byModel = new Map();
|
|
@@ -14,5 +14,10 @@ export function resolveUsageRate(rateCard, modelId, inputTokens = 0, options = {
|
|
|
14
14
|
if (rate.longContext && inputTokens > rate.longContext.thresholdTokens) {
|
|
15
15
|
return rate.longContext.rate;
|
|
16
16
|
}
|
|
17
|
+
if (rate.introOffer &&
|
|
18
|
+
Number.isFinite(options.timestampMs) &&
|
|
19
|
+
options.timestampMs < rate.introOffer.effectiveUntilMs) {
|
|
20
|
+
return rate.introOffer.rate;
|
|
21
|
+
}
|
|
17
22
|
return rate;
|
|
18
23
|
}
|
|
@@ -64,7 +64,11 @@ function resolveReportModelType(stats, window) {
|
|
|
64
64
|
return truncateSchemaString(stats.providerId, 128);
|
|
65
65
|
}
|
|
66
66
|
function resolveClaudeReportModelType(window) {
|
|
67
|
-
|
|
67
|
+
const modelFamily = window.modelType?.toLowerCase().replace(/\s+only$/, "").trim();
|
|
68
|
+
if (!modelFamily) {
|
|
69
|
+
return "all";
|
|
70
|
+
}
|
|
71
|
+
return truncateSchemaString(`${modelFamily.replace(/\s+/g, "-")}-only`, 128);
|
|
68
72
|
}
|
|
69
73
|
function resolveAntigravityReportModelType(stats, window) {
|
|
70
74
|
const limitId = window.limitId.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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",
|
|
8
7
|
"type": "commonjs",
|
|
9
8
|
"bin": {
|
|
10
9
|
"letmecode": "./bin/letmecode.js"
|
|
@@ -21,16 +20,6 @@
|
|
|
21
20
|
"publishConfig": {
|
|
22
21
|
"access": "public"
|
|
23
22
|
},
|
|
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
|
-
},
|
|
34
23
|
"keywords": [
|
|
35
24
|
"cli",
|
|
36
25
|
"ink",
|
|
@@ -46,5 +35,14 @@
|
|
|
46
35
|
"@types/node": "^24.0.7",
|
|
47
36
|
"@types/react": "^18.3.24",
|
|
48
37
|
"typescript": "^5.8.3"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
41
|
+
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
42
|
+
"prestart": "npm run build",
|
|
43
|
+
"start": "node ./bin/letmecode.js",
|
|
44
|
+
"pretest": "npm run build",
|
|
45
|
+
"smoke": "node ./bin/letmecode.js",
|
|
46
|
+
"test": "node --test ink-app/test/*.test.mjs"
|
|
49
47
|
}
|
|
50
|
-
}
|
|
48
|
+
}
|