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
- scope: window.scope,
24
- planType: window.planType,
25
- limitId: window.limitId,
26
- windowMinutes: window.windowMinutes,
27
- startTimeUtcIso: formatIsoFromSeconds(window.minStartsAt),
28
- endTimeUtcIso: formatIsoFromSeconds(window.maxResetsAt),
29
- firstSeenUtcIso: formatIsoFromMilliseconds(window.firstSeenMs),
30
- lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
31
- minUsedPercent: window.minUsedPercent,
32
- maxUsedPercent: window.maxUsedPercent,
33
- totals: computeWindowTotals(window.events),
34
- eventCount: 0
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 computeWindowTotals(events) {
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 totals;
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(stats.providerId, window),
40
+ usage_raw: buildUsageRaw(window.modelUsage),
41
41
  letmecode_version: letmecodeVersion
42
42
  };
43
43
  }
44
- function buildUsageRaw(providerId, window) {
45
- const usageRaw = {
46
- output: window.totals.outputTokens,
47
- input_non_cache: window.totals.inputTokens,
48
- input_cache_read: window.totals.cacheReadInputTokens
49
- };
50
- if (isAnthropicProvider(providerId)) {
51
- usageRaw.input_cache_w5m = window.totals.cacheWrite5mInputTokens;
52
- usageRaw.input_cache_w1h = window.totals.cacheWrite1hInputTokens;
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.7",
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
+ }