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
- readAuthStatusOutput: this.readAuthStatusOutput,
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, subscriptionType] = await Promise.all([
667
+ const [usageOutput, credentials] = await Promise.all([
652
668
  readClaudeUsageCommandOutput(options.root, options.readUsageCommandOutput, options.traceLogger),
653
- readClaudeSubscriptionType(options.root, options.readAuthStatusOutput, options.traceLogger)
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(subscriptionType, snapshots);
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
- function resolveClaudeLivePlanType(subscriptionType, snapshots) {
694
- if (snapshots.some((snapshot) => snapshot.modelScope === "sonnet-only")) {
695
- return "team_premium";
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
- return subscriptionType || "live";
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
- async function readClaudeSubscriptionType(root, override, traceLogger) {
700
- const output = await readClaudeAuthStatusOutput(root, override, traceLogger);
701
- const subscriptionType = parseClaudeSubscriptionType(output);
702
- if (output && !subscriptionType) {
703
- traceClaude(traceLogger, "Could not parse subscription type from auth status output.");
704
- }
705
- traceClaude(traceLogger, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
706
- return subscriptionType;
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 isSonnetOnlyWeek = parsed.label === "week" && parsed.windowQualifier === "sonnet only";
1000
- const limitId = isSonnetOnlyWeek ? "current-week-sonnet-only" : `current-${parsed.label}`;
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
- modelScope: isSonnetOnlyWeek ? "sonnet-only" : "all-models",
1006
- modelType: isSonnetOnlyWeek ? "sonnet only" : undefined,
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.modelScope !== "sonnet-only") {
1164
+ if (!snapshot.modelFamily) {
1091
1165
  return true;
1092
1166
  }
1093
- return modelId.toLowerCase().includes("sonnet");
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
- return window.modelType?.toLowerCase().includes("sonnet") ? "sonnet-only" : "all";
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.21",
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
+ }