letmecode 0.1.0 → 0.1.1

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.
@@ -1,17 +1,45 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useState } from "react";
3
3
  import { Box, Text, useApp, useInput, render } from "ink";
4
4
  import { createProviders } from "./providers/index.js";
5
5
  const VERTICAL_TABS = [
6
- { id: "limit-windows", label: "Limit windows" },
7
- { id: "usage-by-model", label: "Usage by model" }
6
+ { id: "limit-windows", label: "Limits" },
7
+ { id: "summary", label: "Summary" },
8
+ { id: "usage-by-model", label: "by model" }
8
9
  ];
10
+ const CODEX_CREDIT_COST_USD = 0.01;
11
+ const LIMIT_WINDOW_COLUMNS = {
12
+ plan: 8,
13
+ window: 8,
14
+ used: 10,
15
+ date: 17,
16
+ value: 10
17
+ };
18
+ const MODEL_USAGE_COLUMNS = {
19
+ model: 17,
20
+ input: 12,
21
+ cached: 12,
22
+ nonCached: 12,
23
+ output: 11,
24
+ credits: 12,
25
+ value: 12
26
+ };
9
27
  function App() {
10
28
  const { exit } = useApp();
11
29
  const providers = React.useState(() => createProviders())[0];
12
30
  const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
13
31
  const [selectedProviderIndex, setSelectedProviderIndex] = useState(0);
14
32
  const [selectedVerticalTabIndex, setSelectedVerticalTabIndex] = useState(0);
33
+ const [selectedLimitRowIndex, setSelectedLimitRowIndex] = useState(0);
34
+ const [selectedModelRowIndex, setSelectedModelRowIndex] = useState(0);
35
+ const selectedProvider = providerStates[selectedProviderIndex];
36
+ const selectedVerticalTab = VERTICAL_TABS[selectedVerticalTabIndex];
37
+ const limitRows = getLimitRows(selectedProvider);
38
+ const modelRows = getModelRows(selectedProvider);
39
+ const activeLimitRowIndex = clampSelectionIndex(selectedLimitRowIndex, limitRows.length);
40
+ const activeModelRowIndex = clampSelectionIndex(selectedModelRowIndex, modelRows.length);
41
+ const selectedLimitRow = activeLimitRowIndex >= 0 ? limitRows[activeLimitRowIndex] : undefined;
42
+ const selectedModelRow = activeModelRowIndex >= 0 ? modelRows[activeModelRowIndex] : undefined;
15
43
  useEffect(() => {
16
44
  let cancelled = false;
17
45
  for (const provider of providers) {
@@ -44,6 +72,26 @@ function App() {
44
72
  exit();
45
73
  return;
46
74
  }
75
+ if ((key.ctrl || key.shift) && key.downArrow) {
76
+ if (selectedVerticalTab.id === "limit-windows") {
77
+ setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex + 1, limitRows.length));
78
+ return;
79
+ }
80
+ if (selectedVerticalTab.id === "usage-by-model") {
81
+ setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex + 1, modelRows.length));
82
+ return;
83
+ }
84
+ }
85
+ if ((key.ctrl || key.shift) && key.upArrow) {
86
+ if (selectedVerticalTab.id === "limit-windows") {
87
+ setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex - 1, limitRows.length));
88
+ return;
89
+ }
90
+ if (selectedVerticalTab.id === "usage-by-model") {
91
+ setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex - 1, modelRows.length));
92
+ return;
93
+ }
94
+ }
47
95
  if ((input === "\t" && !key.shift) || key.rightArrow) {
48
96
  setSelectedProviderIndex((current) => (current + 1) % providerStates.length);
49
97
  return;
@@ -60,9 +108,7 @@ function App() {
60
108
  setSelectedVerticalTabIndex((current) => (current - 1 + VERTICAL_TABS.length) % VERTICAL_TABS.length);
61
109
  }
62
110
  });
63
- const selectedProvider = providerStates[selectedProviderIndex];
64
- const selectedVerticalTab = VERTICAL_TABS[selectedVerticalTabIndex];
65
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "tab/shift+tab or left/right to switch providers, j/k or up/down for details, q to quit" }), _jsx(Box, { marginTop: 1, children: providerStates.map((state, index) => (_jsx(ProviderTab, { label: state.provider.label, active: index === selectedProviderIndex, status: state.status }, state.provider.id))) }), _jsx(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, paddingY: 0, flexDirection: "column", children: _jsx(SummarySection, { providerState: selectedProvider }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: 22, marginRight: 2, children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id }) })] }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }));
111
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "tab/shift+tab or left/right to switch providers, j/k or up/down for details, ctrl+up/down or shift+up/down to select a row, q to quit" }), _jsx(Box, { marginTop: 1, children: providerStates.map((state, index) => (_jsx(ProviderTab, { label: state.provider.label, active: index === selectedProviderIndex, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: 12, marginRight: 2, children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedModelId: selectedModelRow?.modelId }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRow: selectedLimitRow, selectedModelRow: selectedModelRow }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }));
66
112
  }
67
113
  function ProviderTab(props) {
68
114
  const statusColor = props.status === "error" ? "red" : props.status === "loading" ? "yellow" : "green";
@@ -72,15 +118,10 @@ function ProviderTab(props) {
72
118
  function VerticalTab(props) {
73
119
  return (_jsx(Text, { inverse: props.active, children: props.active ? ` ${props.label} ` : ` ${props.label}` }));
74
120
  }
75
- function SummarySection(props) {
76
- if (props.providerState.status === "loading") {
77
- return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.provider.label }), _jsx(Text, { color: "yellow", children: "Loading stats from local sessions..." })] }));
78
- }
79
- if (props.providerState.status === "error") {
80
- return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.provider.label }), _jsxs(Text, { color: "red", children: ["Failed to load provider stats: ", props.providerState.errorMessage] })] }));
81
- }
82
- const { summary } = props.providerState.stats;
83
- return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.stats.providerLabel }), _jsxs(Text, { children: ["root: ", summary.rootLabel, " (", summary.rootPath, ")"] }), _jsxs(Text, { children: ["files: ", formatInteger(summary.filesScanned), " lines: ", formatInteger(summary.linesRead), " token events: ", formatInteger(summary.tokenEvents)] }), _jsxs(Text, { children: ["input: ", formatInteger(summary.totals.inputTokens), " cached: ", formatInteger(summary.totals.cachedInputTokens), " non-cached: ", formatInteger(summary.totals.nonCachedInputTokens)] }), _jsxs(Text, { children: ["output: ", formatInteger(summary.totals.outputTokens), " reasoning: ", formatInteger(summary.totals.reasoningOutputTokens), " total: ", formatInteger(summary.totals.totalTokens)] }), _jsxs(Text, { children: ["estimated credits: ", formatCredits(summary.totals.estimatedCredits), " models: ", summary.distinctModels.join(", ") || "none", " plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
121
+ function SummaryPanel(props) {
122
+ const { summary } = props.stats;
123
+ const inputPerOutput = formatInputPerOutput(summary.totals);
124
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: props.stats.providerLabel }), _jsxs(Text, { children: ["root: ", summary.rootLabel, " (", summary.rootPath, ")"] }), _jsxs(Text, { children: ["files: ", formatInteger(summary.filesScanned), " lines: ", formatInteger(summary.linesRead), " token events: ", formatInteger(summary.tokenEvents)] }), _jsxs(Text, { children: ["input: ", formatInteger(summary.totals.inputTokens), " cached: ", formatInteger(summary.totals.cachedInputTokens), " non-cached: ", formatInteger(summary.totals.nonCachedInputTokens)] }), _jsxs(Text, { children: ["output: ", formatInteger(summary.totals.outputTokens), " reasoning: ", formatInteger(summary.totals.reasoningOutputTokens), " total: ", formatInteger(summary.totals.totalTokens)] }), _jsxs(Text, { children: ["estimated credits: ", formatCredits(summary.totals.estimatedCredits)] }), _jsxs(Text, { children: ["IpO: ", inputPerOutput.cached, ":", inputPerOutput.nonCached, ":", inputPerOutput.output] }), _jsxs(Text, { children: ["models: ", summary.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
84
125
  }
85
126
  function ContentPanel(props) {
86
127
  if (props.providerState.status === "loading") {
@@ -90,21 +131,25 @@ function ContentPanel(props) {
90
131
  return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
91
132
  }
92
133
  if (props.tabId === "limit-windows") {
93
- return _jsx(LimitWindowsPanel, { stats: props.providerState.stats });
134
+ return _jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey });
94
135
  }
95
- return _jsx(UsageByModelPanel, { stats: props.providerState.stats });
136
+ if (props.tabId === "summary") {
137
+ return _jsx(SummaryPanel, { stats: props.providerState.stats });
138
+ }
139
+ return _jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId });
96
140
  }
97
141
  function LimitWindowsPanel(props) {
98
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Primary" }), _jsx(LimitWindowSection, { windows: props.stats.primaryLimitWindows }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: "Secondary" }), _jsx(LimitWindowSection, { windows: props.stats.secondaryLimitWindows })] }));
142
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Primary" }), _jsx(LimitWindowSection, { windows: props.stats.primaryLimitWindows, selectedRowKey: props.selectedRowKey }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: "Secondary" }), _jsx(LimitWindowSection, { windows: props.stats.secondaryLimitWindows, selectedRowKey: props.selectedRowKey })] }));
99
143
  }
100
144
  function LimitWindowSection(props) {
101
145
  if (props.windows.length === 0) {
102
146
  return _jsx(Text, { color: "gray", children: "No windows found." });
103
147
  }
104
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "plan limit window used start end events" }), props.windows.map((window) => {
148
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("plan", LIMIT_WINDOW_COLUMNS.plan), " ", pad("window", LIMIT_WINDOW_COLUMNS.window), " ", pad("used", LIMIT_WINDOW_COLUMNS.used), " ", pad("start", LIMIT_WINDOW_COLUMNS.date), " ", pad("end", LIMIT_WINDOW_COLUMNS.date), " value"] }), props.windows.map((window) => {
105
149
  const windowLabel = formatWindowMinutes(window.windowMinutes);
106
150
  const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
107
- return (_jsxs(Text, { children: [pad(window.planType, 10), " ", pad(window.limitId, 10), " ", pad(windowLabel, 8), " ", pad(usedLabel, 12), " ", pad(shortIso(window.startTimeIso), 20), " ", pad(shortIso(window.endTimeIso), 20), " ", formatInteger(window.eventCount)] }, `${window.scope}-${window.planType}-${window.limitId}-${window.endTimeIso}`));
151
+ const isSelected = props.selectedRowKey === getLimitRowKey(window);
152
+ return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(window.planType, LIMIT_WINDOW_COLUMNS.plan), " ", pad(windowLabel, LIMIT_WINDOW_COLUMNS.window), " ", pad(usedLabel, LIMIT_WINDOW_COLUMNS.used), " ", pad(formatLocalDateTime(window.startTimeUtcIso), LIMIT_WINDOW_COLUMNS.date), " ", pad(formatLocalDateTime(window.endTimeUtcIso), LIMIT_WINDOW_COLUMNS.date), " ", pad(formatUsd(window.totals.estimatedCredits * CODEX_CREDIT_COST_USD), LIMIT_WINDOW_COLUMNS.value)] }, getLimitRowKey(window)));
108
153
  })] }));
109
154
  }
110
155
  function UsageByModelPanel(props) {
@@ -112,7 +157,28 @@ function UsageByModelPanel(props) {
112
157
  return _jsx(Text, { color: "gray", children: "No model usage found." });
113
158
  }
114
159
  const totals = props.stats.summary.totals;
115
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "model input cached non-cached output credits events" }), props.stats.modelUsage.map((row) => (_jsxs(Text, { children: [pad(row.modelId, 16), " ", pad(formatInteger(row.totals.inputTokens), 12), " ", pad(formatInteger(row.totals.cachedInputTokens), 12), " ", pad(formatInteger(row.totals.nonCachedInputTokens), 12), " ", pad(formatInteger(row.totals.outputTokens), 12), " ", pad(formatCredits(row.totals.estimatedCredits), 12), " ", pad(formatInteger(row.totals.eventCount), 8)] }, row.modelId))), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", 16), " ", pad(formatInteger(totals.inputTokens), 12), " ", pad(formatInteger(totals.cachedInputTokens), 12), " ", pad(formatInteger(totals.nonCachedInputTokens), 12), " ", pad(formatInteger(totals.outputTokens), 12), " ", pad(formatCredits(totals.estimatedCredits), 12), " ", pad(formatInteger(totals.eventCount), 8)] })] }));
160
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("model", MODEL_USAGE_COLUMNS.model), " ", pad("input", MODEL_USAGE_COLUMNS.input), " ", pad("cached", MODEL_USAGE_COLUMNS.cached), " ", pad("non-cached", MODEL_USAGE_COLUMNS.nonCached), " ", pad("output", MODEL_USAGE_COLUMNS.output), " ", pad("credits", MODEL_USAGE_COLUMNS.credits), " value"] }), props.stats.modelUsage.map((row) => {
161
+ const isSelected = props.selectedModelId === row.modelId;
162
+ return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(row.modelId, MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(row.totals.inputTokens), MODEL_USAGE_COLUMNS.input), " ", pad(formatInteger(row.totals.cachedInputTokens), MODEL_USAGE_COLUMNS.cached), " ", pad(formatInteger(row.totals.nonCachedInputTokens), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(row.totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatCredits(row.totals.estimatedCredits), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsd(row.totals.estimatedCredits * CODEX_CREDIT_COST_USD), MODEL_USAGE_COLUMNS.value)] }, row.modelId));
163
+ }), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(totals.inputTokens), MODEL_USAGE_COLUMNS.input), " ", pad(formatInteger(totals.cachedInputTokens), MODEL_USAGE_COLUMNS.cached), " ", pad(formatInteger(totals.nonCachedInputTokens), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatCredits(totals.estimatedCredits), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD), MODEL_USAGE_COLUMNS.value)] })] }));
164
+ }
165
+ function SelectionDetailsPanel(props) {
166
+ if (props.providerState.status !== "ready") {
167
+ return null;
168
+ }
169
+ if (props.tabId === "limit-windows" && props.selectedLimitRow) {
170
+ const row = props.selectedLimitRow;
171
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Limit details" }), _jsxs(Text, { children: [row.scope, " plan: ", row.planType, " window: ", formatWindowMinutes(row.windowMinutes), " used: ", row.minUsedPercent, "%", "->", row.maxUsedPercent, "% limit: ", row.limitId] }), _jsxs(Text, { children: ["range: ", formatLocalDateTime(row.startTimeUtcIso), " ", "->", " ", formatLocalDateTime(row.endTimeUtcIso), " events: ", formatInteger(row.eventCount)] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
172
+ }
173
+ if (props.tabId === "usage-by-model" && props.selectedModelRow) {
174
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Model details" }), _jsxs(Text, { children: ["model: ", props.selectedModelRow.modelId, " events: ", formatInteger(props.selectedModelRow.totals.eventCount)] }), _jsx(UsageTotalsDetails, { totals: props.selectedModelRow.totals })] }));
175
+ }
176
+ return null;
177
+ }
178
+ function UsageTotalsDetails(props) {
179
+ const { totals } = props;
180
+ const inputPerOutput = formatInputPerOutput(totals);
181
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Total credits burned: ", formatCredits(totals.estimatedCredits)] }), _jsxs(Text, { children: ["Credits Value (@ $0.01/credit): ", formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD)] }), _jsxs(Text, { children: ["IpO: ", inputPerOutput.cached, ":", inputPerOutput.nonCached, ":", inputPerOutput.output] })] }));
116
182
  }
117
183
  function formatInteger(value) {
118
184
  return Math.round(value).toLocaleString("en-US");
@@ -123,6 +189,14 @@ function formatCredits(value) {
123
189
  maximumFractionDigits: 2
124
190
  });
125
191
  }
192
+ function formatUsd(value) {
193
+ return value.toLocaleString("en-US", {
194
+ currency: "USD",
195
+ style: "currency",
196
+ minimumFractionDigits: 2,
197
+ maximumFractionDigits: 4
198
+ });
199
+ }
126
200
  function formatWindowMinutes(value) {
127
201
  const hours = value / 60;
128
202
  if (hours >= 24) {
@@ -130,12 +204,53 @@ function formatWindowMinutes(value) {
130
204
  }
131
205
  return `${hours.toFixed(2)}h`;
132
206
  }
133
- function shortIso(value) {
134
- return value.replace(".000Z", "Z").slice(0, 19) + "Z";
207
+ function formatLocalDateTime(value) {
208
+ const parts = new Intl.DateTimeFormat("en-US", {
209
+ month: "short",
210
+ day: "2-digit",
211
+ year: "2-digit",
212
+ hour: "2-digit",
213
+ minute: "2-digit",
214
+ hour12: false,
215
+ hourCycle: "h23"
216
+ }).formatToParts(new Date(value));
217
+ const lookup = Object.fromEntries(parts.map((part) => [part.type, part.value]));
218
+ return `${lookup.day} ${lookup.month} ${lookup.year} ${lookup.hour}:${lookup.minute}`;
135
219
  }
136
220
  function pad(value, length) {
137
221
  return value.length >= length ? value.slice(0, length) : value.padEnd(length);
138
222
  }
223
+ function formatInputPerOutput(totals) {
224
+ if (totals.outputTokens <= 0) {
225
+ return { cached: "0", nonCached: "0", output: "0" };
226
+ }
227
+ return {
228
+ cached: formatInteger(Math.round(totals.cachedInputTokens / totals.outputTokens)),
229
+ nonCached: formatInteger(Math.round(totals.nonCachedInputTokens / totals.outputTokens)),
230
+ output: "1"
231
+ };
232
+ }
233
+ function clampSelectionIndex(value, rowCount) {
234
+ if (rowCount === 0) {
235
+ return -1;
236
+ }
237
+ return Math.max(0, Math.min(value, rowCount - 1));
238
+ }
239
+ function getLimitRows(providerState) {
240
+ if (providerState.status !== "ready") {
241
+ return [];
242
+ }
243
+ return [...providerState.stats.primaryLimitWindows, ...providerState.stats.secondaryLimitWindows];
244
+ }
245
+ function getModelRows(providerState) {
246
+ if (providerState.status !== "ready") {
247
+ return [];
248
+ }
249
+ return providerState.stats.modelUsage;
250
+ }
251
+ function getLimitRowKey(row) {
252
+ return `${row.scope}-${row.planType}-${row.limitId}-${row.startTimeUtcIso}-${row.endTimeUtcIso}`;
253
+ }
139
254
  export function main() {
140
255
  render(_jsx(App, {}));
141
256
  }
@@ -0,0 +1,225 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
6
+ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
7
+ const RATE_CARD = {
8
+ "claude-opus-4-8": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
9
+ "claude-opus-4-7": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
10
+ "claude-opus-4-6": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
11
+ "claude-opus-4-5": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
12
+ "claude-opus-4-1": { input: 1500, cacheWrite5m: 1875, cacheWrite1h: 3000, cacheRead: 150, output: 7500 },
13
+ "claude-opus-4": { input: 1500, cacheWrite5m: 1875, cacheWrite1h: 3000, cacheRead: 150, output: 7500 },
14
+ "claude-sonnet-4-6": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
15
+ "claude-sonnet-4-5": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
16
+ "claude-sonnet-4": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
17
+ "claude-haiku-4-5": { input: 100, cacheWrite5m: 125, cacheWrite1h: 200, cacheRead: 10, output: 500 },
18
+ "claude-haiku-3-5": { input: 80, cacheWrite5m: 100, cacheWrite1h: 160, cacheRead: 8, output: 400 }
19
+ };
20
+ export class ClaudeUsageProvider extends UsageProviderBase {
21
+ constructor(options = {}) {
22
+ super("claude", "Claude");
23
+ this.root = path.resolve(options.root ?? os.homedir());
24
+ }
25
+ async getStats() {
26
+ const sessionsRoot = path.join(this.root, ".claude", "projects");
27
+ const byModel = new Map();
28
+ const windows = createLimitWindowAggregates();
29
+ const planTypes = new Set();
30
+ const warnings = [];
31
+ const seenUsageEvents = new Set();
32
+ const parseTotals = {
33
+ filesScanned: 0,
34
+ linesRead: 0,
35
+ tokenEvents: 0,
36
+ malformedLines: 0
37
+ };
38
+ for await (const file of walkSessionFiles(sessionsRoot)) {
39
+ parseTotals.filesScanned += 1;
40
+ const fileStats = await parseSessionFile(file, byModel, windows, planTypes, seenUsageEvents);
41
+ parseTotals.linesRead += fileStats.linesRead;
42
+ parseTotals.tokenEvents += fileStats.tokenEvents;
43
+ parseTotals.malformedLines += fileStats.malformedLines;
44
+ }
45
+ if (parseTotals.malformedLines > 0) {
46
+ warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
47
+ }
48
+ const modelUsage = [...byModel.entries()]
49
+ .map(([modelId, totals]) => ({ modelId, totals }))
50
+ .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
51
+ const unknownPricedModels = modelUsage
52
+ .map((row) => row.modelId)
53
+ .filter((modelId) => !resolveRate(modelId));
54
+ if (unknownPricedModels.length > 0) {
55
+ warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
56
+ }
57
+ if (parseTotals.filesScanned === 0) {
58
+ warnings.push(`No Claude session files found under ${sessionsRoot}.`);
59
+ }
60
+ const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
61
+ const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
62
+ if (parseTotals.filesScanned > 0 &&
63
+ parseTotals.tokenEvents > 0 &&
64
+ primaryLimitWindows.length === 0 &&
65
+ secondaryLimitWindows.length === 0) {
66
+ warnings.push("Claude transcripts did not expose rate-limit windows in the local logs.");
67
+ }
68
+ return {
69
+ providerId: this.id,
70
+ providerLabel: this.label,
71
+ summary: {
72
+ filesScanned: parseTotals.filesScanned,
73
+ linesRead: parseTotals.linesRead,
74
+ tokenEvents: parseTotals.tokenEvents,
75
+ totals: summaryTotals,
76
+ distinctModels: modelUsage.map((row) => row.modelId),
77
+ distinctPlanTypes: [...planTypes].sort(),
78
+ rootLabel: "~/.claude/projects",
79
+ rootPath: sessionsRoot
80
+ },
81
+ modelUsage,
82
+ primaryLimitWindows,
83
+ secondaryLimitWindows,
84
+ warnings
85
+ };
86
+ }
87
+ }
88
+ function normalizeUsage(value) {
89
+ const usage = asRecord(value) ?? {};
90
+ const cacheCreation = asRecord(usage.cache_creation);
91
+ const cacheCreation5mInputTokens = numberOrZero(cacheCreation?.ephemeral_5m_input_tokens);
92
+ const cacheCreation1hInputTokens = numberOrZero(cacheCreation?.ephemeral_1h_input_tokens);
93
+ const cacheCreationInputTokens = Math.max(numberOrZero(usage.cache_creation_input_tokens), cacheCreation5mInputTokens + cacheCreation1hInputTokens);
94
+ return {
95
+ inputTokens: numberOrZero(usage.input_tokens),
96
+ cacheReadInputTokens: numberOrZero(usage.cache_read_input_tokens),
97
+ cacheCreationInputTokens,
98
+ cacheCreation5mInputTokens,
99
+ cacheCreation1hInputTokens,
100
+ outputTokens: numberOrZero(usage.output_tokens),
101
+ inferenceGeo: String(usage.inference_geo ?? "")
102
+ };
103
+ }
104
+ function resolveRate(modelId) {
105
+ const candidates = Object.keys(RATE_CARD).sort((left, right) => right.length - left.length);
106
+ for (const candidate of candidates) {
107
+ if (modelId === candidate || modelId.startsWith(`${candidate}-`)) {
108
+ return RATE_CARD[candidate];
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+ function creditsFor(modelId, usage) {
114
+ const rate = resolveRate(modelId);
115
+ if (!rate) {
116
+ return 0;
117
+ }
118
+ const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
119
+ const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
120
+ const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
121
+ return (((usage.inputTokens / 1000000) * rate.input +
122
+ (usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
123
+ (usage.cacheCreation5mInputTokens / 1000000) * rate.cacheWrite5m +
124
+ (usage.cacheCreation1hInputTokens / 1000000) * rate.cacheWrite1h +
125
+ (cacheWriteFallbackTokens / 1000000) * rate.cacheWrite5m +
126
+ (usage.outputTokens / 1000000) * rate.output) *
127
+ inferenceMultiplier);
128
+ }
129
+ function usageToTotals(modelId, usage) {
130
+ const nonCachedInputTokens = usage.inputTokens + usage.cacheCreationInputTokens;
131
+ const cachedInputTokens = usage.cacheReadInputTokens;
132
+ return {
133
+ inputTokens: nonCachedInputTokens + cachedInputTokens,
134
+ cachedInputTokens,
135
+ nonCachedInputTokens,
136
+ outputTokens: usage.outputTokens,
137
+ reasoningOutputTokens: 0,
138
+ totalTokens: nonCachedInputTokens + cachedInputTokens + usage.outputTokens,
139
+ estimatedCredits: creditsFor(modelId, usage),
140
+ eventCount: 1
141
+ };
142
+ }
143
+ function addModelUsage(byModel, modelId, deltaTotals) {
144
+ const resolvedModelId = modelId || "unknown";
145
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
146
+ addUsageTotals(totals, deltaTotals);
147
+ byModel.set(resolvedModelId, totals);
148
+ }
149
+ function isSessionFile(filePath) {
150
+ return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
151
+ }
152
+ async function* walkSessionFiles(directory) {
153
+ let entries;
154
+ try {
155
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
156
+ }
157
+ catch {
158
+ return;
159
+ }
160
+ for (const entry of entries) {
161
+ const fullPath = path.join(directory, entry.name);
162
+ if (entry.isDirectory()) {
163
+ yield* walkSessionFiles(fullPath);
164
+ }
165
+ else if (entry.isFile() && isSessionFile(fullPath)) {
166
+ yield fullPath;
167
+ }
168
+ }
169
+ }
170
+ async function parseSessionFile(filePath, byModel, windows, planTypes, seenUsageEvents) {
171
+ const stream = fs.createReadStream(filePath, { encoding: "utf8" });
172
+ const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
173
+ let linesRead = 0;
174
+ let tokenEvents = 0;
175
+ let malformedLines = 0;
176
+ for await (const line of lineReader) {
177
+ linesRead += 1;
178
+ if (!line.trim()) {
179
+ continue;
180
+ }
181
+ let payloadObject;
182
+ try {
183
+ payloadObject = JSON.parse(line);
184
+ }
185
+ catch {
186
+ malformedLines += 1;
187
+ continue;
188
+ }
189
+ if (payloadObject.type !== "assistant") {
190
+ continue;
191
+ }
192
+ const message = asRecord(payloadObject.message);
193
+ const usage = asRecord(message?.usage);
194
+ if (!usage) {
195
+ continue;
196
+ }
197
+ const usageKey = buildUsageEventKey(payloadObject, message);
198
+ if (usageKey && seenUsageEvents.has(usageKey)) {
199
+ continue;
200
+ }
201
+ if (usageKey) {
202
+ seenUsageEvents.add(usageKey);
203
+ }
204
+ const modelId = String(message?.model ?? "unknown");
205
+ const deltaTotals = usageToTotals(modelId, normalizeUsage(usage));
206
+ addModelUsage(byModel, modelId, deltaTotals);
207
+ tokenEvents += 1;
208
+ const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
209
+ const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
210
+ applyRateLimits(windows, extractRateLimits(payloadObject, message), safeEventTimeMs, deltaTotals, planTypes);
211
+ }
212
+ return { linesRead, tokenEvents, malformedLines };
213
+ }
214
+ function buildUsageEventKey(payloadObject, message) {
215
+ const sessionId = String(payloadObject.sessionId ?? "");
216
+ const requestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : "";
217
+ const messageId = typeof message?.id === "string" ? message.id : "";
218
+ if (!requestId && !messageId) {
219
+ return null;
220
+ }
221
+ return `${sessionId}|${requestId || messageId}`;
222
+ }
223
+ function extractRateLimits(payloadObject, message) {
224
+ return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
225
+ }
@@ -2,7 +2,8 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import readline from "node:readline";
5
- import { UsageProviderBase, createEmptyUsageTotals } from "./contract.js";
5
+ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
6
+ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
6
7
  const RATE_CARD = {
7
8
  "gpt-5.5": { input: 125, cachedInput: 12.5, output: 750 },
8
9
  "gpt-5.4": { input: 62.5, cachedInput: 6.25, output: 375 },
@@ -16,7 +17,7 @@ export class CodexUsageProvider extends UsageProviderBase {
16
17
  async getStats() {
17
18
  const sessionsRoot = path.join(this.root, ".codex", "sessions");
18
19
  const byModel = new Map();
19
- const windows = new Map();
20
+ const windows = createLimitWindowAggregates();
20
21
  const planTypes = new Set();
21
22
  const warnings = [];
22
23
  const parseTotals = {
@@ -78,9 +79,6 @@ function createEmptyRawUsage() {
78
79
  totalTokens: 0
79
80
  };
80
81
  }
81
- function numberOrZero(value) {
82
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
83
- }
84
82
  function normalizeRawUsage(value) {
85
83
  const usage = value && typeof value === "object" ? value : {};
86
84
  return {
@@ -124,104 +122,19 @@ function rawUsageToTotals(usage) {
124
122
  eventCount: 0
125
123
  };
126
124
  }
127
- function addUsageTotals(target, source) {
128
- target.inputTokens += source.inputTokens;
129
- target.cachedInputTokens += source.cachedInputTokens;
130
- target.nonCachedInputTokens += source.nonCachedInputTokens;
131
- target.outputTokens += source.outputTokens;
132
- target.reasoningOutputTokens += source.reasoningOutputTokens;
133
- target.totalTokens += source.totalTokens;
134
- target.estimatedCredits += source.estimatedCredits;
135
- target.eventCount += source.eventCount;
136
- }
137
- function addModelUsage(byModel, modelId, usage) {
125
+ function createUsageTotalsForModel(modelId, usage) {
138
126
  const resolvedModelId = modelId || "unknown";
139
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
140
127
  const deltaTotals = rawUsageToTotals(usage);
141
128
  deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
142
129
  deltaTotals.eventCount = 1;
130
+ return deltaTotals;
131
+ }
132
+ function addModelUsage(byModel, modelId, deltaTotals) {
133
+ const resolvedModelId = modelId || "unknown";
134
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
143
135
  addUsageTotals(totals, deltaTotals);
144
136
  byModel.set(resolvedModelId, totals);
145
137
  }
146
- function sumUsageTotals(rows) {
147
- const totals = createEmptyUsageTotals();
148
- for (const row of rows) {
149
- addUsageTotals(totals, row);
150
- }
151
- return totals;
152
- }
153
- function formatIsoFromSeconds(seconds) {
154
- return new Date(seconds * 1000).toISOString().replace(".000Z", "Z");
155
- }
156
- function formatIsoFromMilliseconds(milliseconds) {
157
- return new Date(milliseconds).toISOString().replace(".000Z", "Z");
158
- }
159
- function makeWindowKey(scope, rateLimits, window) {
160
- return [
161
- scope,
162
- String(rateLimits.limit_id ?? "unknown"),
163
- String(rateLimits.plan_type ?? "unknown"),
164
- numberOrZero(window.window_minutes),
165
- Math.round(numberOrZero(window.resets_at) / 60)
166
- ].join("|");
167
- }
168
- function upsertWindow(windows, scope, rateLimits, window, eventTimeMs) {
169
- if (!window) {
170
- return;
171
- }
172
- const windowMinutes = numberOrZero(window.window_minutes);
173
- const resetsAt = numberOrZero(window.resets_at);
174
- if (!windowMinutes || !resetsAt) {
175
- return;
176
- }
177
- const startsAt = resetsAt - windowMinutes * 60;
178
- const usedPercent = numberOrZero(window.used_percent);
179
- const key = makeWindowKey(scope, rateLimits, window);
180
- const existing = windows.get(key);
181
- if (!existing) {
182
- windows.set(key, {
183
- scope,
184
- limitId: String(rateLimits.limit_id ?? "unknown"),
185
- planType: String(rateLimits.plan_type ?? "unknown"),
186
- windowMinutes,
187
- minStartsAt: startsAt,
188
- maxResetsAt: resetsAt,
189
- firstSeenMs: eventTimeMs,
190
- lastSeenMs: eventTimeMs,
191
- minUsedPercent: usedPercent,
192
- maxUsedPercent: usedPercent,
193
- eventCount: 1
194
- });
195
- return;
196
- }
197
- existing.minStartsAt = Math.min(existing.minStartsAt, startsAt);
198
- existing.maxResetsAt = Math.max(existing.maxResetsAt, resetsAt);
199
- existing.firstSeenMs = Math.min(existing.firstSeenMs, eventTimeMs);
200
- existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
201
- existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
202
- existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
203
- existing.eventCount += 1;
204
- }
205
- function buildWindowLists(windows) {
206
- const rows = [...windows.values()]
207
- .map((window) => ({
208
- scope: window.scope,
209
- planType: window.planType,
210
- limitId: window.limitId,
211
- windowMinutes: window.windowMinutes,
212
- startTimeIso: formatIsoFromSeconds(window.minStartsAt),
213
- endTimeIso: formatIsoFromSeconds(window.maxResetsAt),
214
- firstSeenIso: formatIsoFromMilliseconds(window.firstSeenMs),
215
- lastSeenIso: formatIsoFromMilliseconds(window.lastSeenMs),
216
- minUsedPercent: window.minUsedPercent,
217
- maxUsedPercent: window.maxUsedPercent,
218
- eventCount: window.eventCount
219
- }))
220
- .sort((left, right) => right.endTimeIso.localeCompare(left.endTimeIso));
221
- const primary = rows.filter((row) => row.scope === "primary").slice(0, 5);
222
- const secondary = rows.filter((row) => row.scope === "secondary").slice(0, 5);
223
- return [primary, secondary];
224
- }
225
138
  function isSessionFile(filePath) {
226
139
  return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
227
140
  }
@@ -279,23 +192,17 @@ async function parseSessionFile(filePath, byModel, windows, planTypes) {
279
192
  continue;
280
193
  }
281
194
  const info = payload.info;
282
- const rateLimits = payload.rate_limits;
283
195
  const totalUsage = normalizeRawUsage(info?.total_token_usage);
284
196
  const lastUsage = info?.last_token_usage;
285
197
  const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
286
198
  previousTotal = totalUsage;
199
+ const resolvedModelId = currentModel || "unknown";
200
+ const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage);
287
201
  tokenEvents += 1;
288
- addModelUsage(byModel, currentModel, usage);
289
- if (typeof rateLimits?.plan_type === "string") {
290
- planTypes.add(rateLimits.plan_type);
291
- }
202
+ addModelUsage(byModel, resolvedModelId, deltaTotals);
292
203
  const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
293
204
  const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
294
- upsertWindow(windows, "primary", rateLimits ?? {}, asRecord(rateLimits?.primary), safeEventTimeMs);
295
- upsertWindow(windows, "secondary", rateLimits ?? {}, asRecord(rateLimits?.secondary), safeEventTimeMs);
205
+ applyRateLimits(windows, asRecord(payload.rate_limits), safeEventTimeMs, deltaTotals, planTypes);
296
206
  }
297
207
  return { linesRead, tokenEvents, malformedLines };
298
208
  }
299
- function asRecord(value) {
300
- return value && typeof value === "object" ? value : null;
301
- }
@@ -16,3 +16,20 @@ export function createEmptyUsageTotals() {
16
16
  eventCount: 0
17
17
  };
18
18
  }
19
+ export function addUsageTotals(target, source) {
20
+ target.inputTokens += source.inputTokens;
21
+ target.cachedInputTokens += source.cachedInputTokens;
22
+ target.nonCachedInputTokens += source.nonCachedInputTokens;
23
+ target.outputTokens += source.outputTokens;
24
+ target.reasoningOutputTokens += source.reasoningOutputTokens;
25
+ target.totalTokens += source.totalTokens;
26
+ target.estimatedCredits += source.estimatedCredits;
27
+ target.eventCount += source.eventCount;
28
+ }
29
+ export function sumUsageTotals(rows) {
30
+ const totals = createEmptyUsageTotals();
31
+ for (const row of rows) {
32
+ addUsageTotals(totals, row);
33
+ }
34
+ return totals;
35
+ }
@@ -1,6 +1,8 @@
1
+ import { ClaudeUsageProvider } from "./claude.js";
1
2
  import { CodexUsageProvider } from "./codex.js";
2
3
  export function createProviders() {
3
- return [new CodexUsageProvider()];
4
+ return [new CodexUsageProvider(), new ClaudeUsageProvider()];
4
5
  }
6
+ export { ClaudeUsageProvider } from "./claude.js";
5
7
  export { CodexUsageProvider } from "./codex.js";
6
8
  export { UsageProviderBase } from "./contract.js";
@@ -0,0 +1,146 @@
1
+ import { addUsageTotals, createEmptyUsageTotals } from "./contract.js";
2
+ export function createLimitWindowAggregates() {
3
+ return new Map();
4
+ }
5
+ export function numberOrZero(value) {
6
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
7
+ }
8
+ export function asRecord(value) {
9
+ return value && typeof value === "object" ? value : null;
10
+ }
11
+ export function applyRateLimits(windows, rateLimits, eventTimeMs, deltaTotals, planTypes) {
12
+ if (!rateLimits) {
13
+ return;
14
+ }
15
+ if (typeof rateLimits.plan_type === "string") {
16
+ planTypes.add(rateLimits.plan_type);
17
+ }
18
+ upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, deltaTotals);
19
+ upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, deltaTotals);
20
+ }
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
+ })))
36
+ .map((row) => ({
37
+ ...row,
38
+ eventCount: row.totals.eventCount
39
+ }))
40
+ .sort((left, right) => right.endTimeUtcIso.localeCompare(left.endTimeUtcIso));
41
+ const primary = rows.filter((row) => row.scope === "primary").slice(0, 5);
42
+ const secondary = rows.filter((row) => row.scope === "secondary").slice(0, 5);
43
+ return [primary, secondary];
44
+ }
45
+ function formatIsoFromSeconds(seconds) {
46
+ return new Date(seconds * 1000).toISOString().replace(".000Z", "Z");
47
+ }
48
+ function formatIsoFromMilliseconds(milliseconds) {
49
+ return new Date(milliseconds).toISOString().replace(".000Z", "Z");
50
+ }
51
+ function makeWindowKey(scope, rateLimits, window) {
52
+ return [
53
+ scope,
54
+ String(rateLimits.limit_id ?? "unknown"),
55
+ String(rateLimits.plan_type ?? "unknown"),
56
+ numberOrZero(window.window_minutes),
57
+ numberOrZero(window.resets_at)
58
+ ].join("|");
59
+ }
60
+ function collapseNearbyWindows(rows) {
61
+ const collapsed = new Map();
62
+ for (const row of rows) {
63
+ const key = [
64
+ row.scope,
65
+ row.limitId,
66
+ row.planType,
67
+ row.windowMinutes,
68
+ Math.round(Date.parse(row.endTimeUtcIso) / 60000)
69
+ ].join("|");
70
+ const existing = collapsed.get(key);
71
+ if (!existing) {
72
+ collapsed.set(key, {
73
+ ...row,
74
+ totals: { ...row.totals }
75
+ });
76
+ continue;
77
+ }
78
+ existing.startTimeUtcIso =
79
+ existing.startTimeUtcIso < row.startTimeUtcIso ? existing.startTimeUtcIso : row.startTimeUtcIso;
80
+ existing.endTimeUtcIso =
81
+ existing.endTimeUtcIso > row.endTimeUtcIso ? existing.endTimeUtcIso : row.endTimeUtcIso;
82
+ existing.firstSeenUtcIso =
83
+ existing.firstSeenUtcIso < row.firstSeenUtcIso ? existing.firstSeenUtcIso : row.firstSeenUtcIso;
84
+ existing.lastSeenUtcIso =
85
+ existing.lastSeenUtcIso > row.lastSeenUtcIso ? existing.lastSeenUtcIso : row.lastSeenUtcIso;
86
+ existing.minUsedPercent = Math.min(existing.minUsedPercent, row.minUsedPercent);
87
+ existing.maxUsedPercent = Math.max(existing.maxUsedPercent, row.maxUsedPercent);
88
+ addUsageTotals(existing.totals, row.totals);
89
+ existing.eventCount = existing.totals.eventCount;
90
+ }
91
+ return [...collapsed.values()];
92
+ }
93
+ function computeWindowTotals(events) {
94
+ // Session files are not guaranteed to be parsed in timestamp order, so
95
+ // saturation has to be applied after we sort the captured window events.
96
+ const totals = createEmptyUsageTotals();
97
+ let sawBelowCap = false;
98
+ let isExhausted = false;
99
+ for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
100
+ sawBelowCap || (sawBelowCap = event.usedPercent < 100);
101
+ if (!isExhausted) {
102
+ addUsageTotals(totals, event.totals);
103
+ if (sawBelowCap && event.usedPercent >= 100) {
104
+ isExhausted = true;
105
+ }
106
+ }
107
+ }
108
+ return totals;
109
+ }
110
+ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTotals) {
111
+ if (!window) {
112
+ return;
113
+ }
114
+ const windowMinutes = numberOrZero(window.window_minutes);
115
+ const resetsAt = numberOrZero(window.resets_at);
116
+ if (!windowMinutes || !resetsAt) {
117
+ return;
118
+ }
119
+ const startsAt = resetsAt - windowMinutes * 60;
120
+ const usedPercent = numberOrZero(window.used_percent);
121
+ const key = makeWindowKey(scope, rateLimits, window);
122
+ const existing = windows.get(key);
123
+ if (!existing) {
124
+ windows.set(key, {
125
+ scope,
126
+ limitId: String(rateLimits.limit_id ?? "unknown"),
127
+ planType: String(rateLimits.plan_type ?? "unknown"),
128
+ windowMinutes,
129
+ minStartsAt: startsAt,
130
+ maxResetsAt: resetsAt,
131
+ firstSeenMs: eventTimeMs,
132
+ lastSeenMs: eventTimeMs,
133
+ minUsedPercent: usedPercent,
134
+ maxUsedPercent: usedPercent,
135
+ events: [{ eventTimeMs, usedPercent, totals: { ...deltaTotals } }]
136
+ });
137
+ return;
138
+ }
139
+ existing.minStartsAt = Math.min(existing.minStartsAt, startsAt);
140
+ existing.maxResetsAt = Math.max(existing.maxResetsAt, resetsAt);
141
+ existing.firstSeenMs = Math.min(existing.firstSeenMs, eventTimeMs);
142
+ existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
143
+ existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
144
+ existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
145
+ existing.events.push({ eventTimeMs, usedPercent, totals: { ...deltaTotals } });
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",