letmecode 0.1.2 → 0.1.4

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,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useEffect, useState } from "react";
3
- import { Box, Text, useApp, useInput, render } from "ink";
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, measureElement, useApp, useInput, useStdout, render } from "ink";
4
4
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
5
5
  const VERTICAL_TABS = [
6
6
  { id: "limit-windows", label: "Limits" },
@@ -17,27 +17,52 @@ const LIMIT_WINDOW_COLUMNS = {
17
17
  date: 17,
18
18
  value: 10
19
19
  };
20
- const MODEL_USAGE_COLUMNS = {
20
+ const OPENAI_MODEL_USAGE_COLUMNS = {
21
21
  model: 17,
22
22
  input: 12,
23
23
  cached: 12,
24
- nonCached: 12,
25
24
  output: 11,
26
25
  credits: 12,
27
26
  value: 12
28
27
  };
29
- const DAY_USAGE_COLUMNS = {
28
+ const ANTHROPIC_MODEL_USAGE_COLUMNS = {
29
+ model: 17,
30
+ input: 10,
31
+ cacheWrite5m: 10,
32
+ cacheWrite1h: 10,
33
+ cacheRead: 10,
34
+ output: 10,
35
+ credits: 12,
36
+ value: 12
37
+ };
38
+ const OPENAI_DAY_USAGE_COLUMNS = {
30
39
  day: 11,
31
40
  events: 6,
32
41
  input: 11,
33
42
  output: 10,
34
43
  value: 10
35
44
  };
45
+ const ANTHROPIC_DAY_USAGE_COLUMNS = {
46
+ day: 11,
47
+ events: 6,
48
+ input: 10,
49
+ cacheWrite5m: 10,
50
+ cacheWrite1h: 10,
51
+ cacheRead: 10,
52
+ output: 10,
53
+ value: 10
54
+ };
36
55
  const COPILOT_ACTIONS = [
37
56
  { id: "vscode", label: "Start logging VS Code", enabled: true }
38
57
  ];
58
+ const ENTER_FULLSCREEN_MODE = "\u001B[?1049h\u001B[2J\u001B[H";
59
+ const EXIT_FULLSCREEN_MODE = "\u001B[?1049l";
60
+ const SCROLLBAR_TRACK_GLYPH = "│";
61
+ const SCROLLBAR_THUMB_GLYPH = "█";
39
62
  function App(props) {
40
63
  const { exit } = useApp();
64
+ const viewportHeight = useViewportHeight();
65
+ const { ref: contentPanelRef, height: contentPanelHeight } = useMeasuredElementSize();
41
66
  const providers = React.useState(() => createProviders())[0];
42
67
  const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
43
68
  const [selectedProviderId, setSelectedProviderId] = useState(providers[0]?.id ?? "");
@@ -166,7 +191,7 @@ function App(props) {
166
191
  setSelectedVerticalTabIndex((current) => (current - 1 + VERTICAL_TABS.length) % VERTICAL_TABS.length);
167
192
  }
168
193
  });
169
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "[/]/tab to switch providers, j/k or up/down for details, left/right to select a row, enter for actions, q to quit" }), _jsx(Box, { marginTop: 1, children: sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: VERTICAL_TAB_WIDTH, 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, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), 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] }));
194
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "[/]/tab to switch providers, j/k or up/down for details, left/right to select a row, enter for actions, q to quit" }), _jsx(Box, { marginTop: 1, children: sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", width: VERTICAL_TAB_WIDTH, marginRight: 2, overflow: "hidden", children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] })] }));
170
195
  }
171
196
  function CopilotActionsPanel(props) {
172
197
  if (props.providerState.provider.id !== "copilot") {
@@ -210,8 +235,7 @@ function VerticalTab(props) {
210
235
  }
211
236
  function SummaryPanel(props) {
212
237
  const { summary } = props.stats;
213
- const inputPerOutput = formatInputPerOutput(summary.totals);
214
- 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: ", formatCacheTokens(summary.totals, "cached"), " non-cached: ", formatCacheTokens(summary.totals, "non-cached")] }), _jsxs(Text, { children: ["output: ", formatInteger(summary.totals.outputTokens), " reasoning: ", formatInteger(summary.totals.reasoningOutputTokens), " total: ", formatInteger(summary.totals.totalTokens)] }), _jsxs(Text, { children: ["estimated credits: ", formatUsageCredits(summary.totals)] }), _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"] })] }));
238
+ 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)] }), _jsx(UsageBreakdownLines, { totals: summary.totals }), _jsxs(Text, { children: ["estimated credits: ", formatUsageCredits(summary.totals)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(summary.totals)] }), _jsxs(Text, { children: ["models: ", summary.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
215
239
  }
216
240
  function ContentPanel(props) {
217
241
  if (props.providerState.status === "loading") {
@@ -221,48 +245,171 @@ function ContentPanel(props) {
221
245
  return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
222
246
  }
223
247
  if (props.tabId === "limit-windows") {
224
- return _jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey });
248
+ return (_jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey, availableHeight: props.availableHeight }));
225
249
  }
226
250
  if (props.tabId === "summary") {
227
251
  return _jsx(SummaryPanel, { stats: props.providerState.stats });
228
252
  }
229
253
  if (props.tabId === "day-to-day-analyses") {
230
- return _jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey });
254
+ return (_jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey, availableHeight: props.availableHeight }));
231
255
  }
232
- return _jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId });
256
+ return (_jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId, availableHeight: props.availableHeight }));
233
257
  }
234
258
  function LimitWindowsPanel(props) {
235
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Primary Limit Windows" }), _jsx(LimitWindowSection, { windows: props.stats.primaryLimitWindows, selectedRowKey: props.selectedRowKey }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: "Secondary Limit Windows" }), _jsx(LimitWindowSection, { windows: props.stats.secondaryLimitWindows, selectedRowKey: props.selectedRowKey })] }));
236
- }
237
- function LimitWindowSection(props) {
238
- if (props.windows.length === 0) {
239
- return _jsx(Text, { color: "gray", children: "No windows found." });
240
- }
241
- 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) => {
242
- const windowLabel = formatWindowMinutes(window.windowMinutes);
243
- const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
244
- const isSelected = props.selectedRowKey === getLimitRowKey(window);
245
- 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)));
246
- })] }));
259
+ const bodyLines = [
260
+ { key: "primary-title", text: "Primary Limit Windows", bold: true },
261
+ ...buildLimitWindowSectionLines("primary", props.stats.primaryLimitWindows, props.selectedRowKey),
262
+ { key: "section-gap", text: "" },
263
+ { key: "secondary-title", text: "Secondary Limit Windows", bold: true },
264
+ ...buildLimitWindowSectionLines("secondary", props.stats.secondaryLimitWindows, props.selectedRowKey)
265
+ ];
266
+ return (_jsx(ScrollableLineViewport, { bodyLines: bodyLines, selectedBodyLineKey: props.selectedRowKey ? `limit-row:${props.selectedRowKey}` : undefined, availableHeight: props.availableHeight }));
247
267
  }
248
268
  function UsageByModelPanel(props) {
249
269
  if (props.stats.modelUsage.length === 0) {
250
270
  return _jsx(Text, { color: "gray", children: "No model usage found." });
251
271
  }
252
272
  const totals = props.stats.summary.totals;
253
- 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) => {
254
- const isSelected = props.selectedModelId === row.modelId;
255
- 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(formatCacheTokens(row.totals, "cached"), MODEL_USAGE_COLUMNS.cached), " ", pad(formatCacheTokens(row.totals, "non-cached"), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(row.totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(row.totals), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(row.totals), MODEL_USAGE_COLUMNS.value)] }, row.modelId));
256
- }), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(totals.inputTokens), MODEL_USAGE_COLUMNS.input), " ", pad(formatCacheTokens(totals, "cached"), MODEL_USAGE_COLUMNS.cached), " ", pad(formatCacheTokens(totals, "non-cached"), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(totals), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(totals), MODEL_USAGE_COLUMNS.value)] })] }));
273
+ if (totals.tokenBreakdown.schema === "anthropic") {
274
+ return (_jsx(ScrollableLineViewport, { headerLines: [
275
+ {
276
+ key: "anthropic-model-header",
277
+ text: `${pad("model", ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad("input", ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad("cacheW5m", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad("cacheRead", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad("output", ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad("credits", ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} value`,
278
+ color: "gray"
279
+ }
280
+ ], bodyLines: props.stats.modelUsage.flatMap((row) => {
281
+ if (row.totals.tokenBreakdown.schema !== "anthropic") {
282
+ return [];
283
+ }
284
+ return [
285
+ {
286
+ key: `model-row:${row.modelId}`,
287
+ text: `${pad(row.modelId, ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(row.totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(row.totals, row.modelId), ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(row.totals, row.modelId), ANTHROPIC_MODEL_USAGE_COLUMNS.value)}`,
288
+ inverse: props.selectedModelId === row.modelId,
289
+ color: props.selectedModelId === row.modelId ? "cyan" : undefined
290
+ }
291
+ ];
292
+ }), footerLines: [
293
+ {
294
+ key: "anthropic-model-total",
295
+ text: `${pad("TOTAL", ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad(formatInteger(totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad(formatInteger(totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.value)}`,
296
+ color: "cyan"
297
+ }
298
+ ], selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
299
+ }
300
+ return (_jsx(ScrollableLineViewport, { headerLines: [
301
+ {
302
+ key: "openai-model-header",
303
+ text: `${pad("model", OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad("uncached", OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad("cached", OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad("output", OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad("credits", OPENAI_MODEL_USAGE_COLUMNS.credits)} value`,
304
+ color: "gray"
305
+ }
306
+ ], bodyLines: props.stats.modelUsage.flatMap((row) => {
307
+ if (row.totals.tokenBreakdown.schema !== "openai") {
308
+ return [];
309
+ }
310
+ return [
311
+ {
312
+ key: `model-row:${row.modelId}`,
313
+ text: `${pad(row.modelId, OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad(formatOpenAiTokens(row.totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad(formatOpenAiTokens(row.totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad(formatInteger(row.totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(row.totals, row.modelId), OPENAI_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(row.totals, row.modelId), OPENAI_MODEL_USAGE_COLUMNS.value)}`,
314
+ inverse: props.selectedModelId === row.modelId,
315
+ color: props.selectedModelId === row.modelId ? "cyan" : undefined
316
+ }
317
+ ];
318
+ }), footerLines: [
319
+ {
320
+ key: "openai-model-total",
321
+ text: `${pad("TOTAL", OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad(formatOpenAiTokens(totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad(formatOpenAiTokens(totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad(formatInteger(totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(totals), OPENAI_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(totals), OPENAI_MODEL_USAGE_COLUMNS.value)}`,
322
+ color: "cyan"
323
+ }
324
+ ], selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
257
325
  }
258
326
  function DayToDayPanel(props) {
259
327
  if (props.stats.dayUsage.length === 0) {
260
328
  return _jsx(Text, { color: "gray", children: "No day-by-day usage found." });
261
329
  }
262
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("day", DAY_USAGE_COLUMNS.day), " ", pad("events", DAY_USAGE_COLUMNS.events), " ", pad("input", DAY_USAGE_COLUMNS.input), " ", pad("output", DAY_USAGE_COLUMNS.output), " value"] }), props.stats.dayUsage.map((row) => {
263
- const isSelected = props.selectedDayKey === row.dayKey;
264
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(formatUtcDay(row.dayKey), DAY_USAGE_COLUMNS.day), " ", pad(formatInteger(row.totals.eventCount), DAY_USAGE_COLUMNS.events), " ", pad(formatInteger(row.totals.inputTokens), DAY_USAGE_COLUMNS.input), " ", pad(formatInteger(row.totals.outputTokens), DAY_USAGE_COLUMNS.output), " ", pad(formatUsageUsd(row.totals), DAY_USAGE_COLUMNS.value)] }, row.dayKey));
265
- })] }));
330
+ const totals = props.stats.summary.totals;
331
+ if (totals.tokenBreakdown.schema === "anthropic") {
332
+ return (_jsx(ScrollableLineViewport, { headerLines: [
333
+ {
334
+ key: "anthropic-day-header",
335
+ text: `${pad("day", ANTHROPIC_DAY_USAGE_COLUMNS.day)} ${pad("events", ANTHROPIC_DAY_USAGE_COLUMNS.events)} ${pad("input", ANTHROPIC_DAY_USAGE_COLUMNS.input)} ${pad("cacheW5m", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h)} ${pad("cacheRead", ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead)} ${pad("output", ANTHROPIC_DAY_USAGE_COLUMNS.output)} value`,
336
+ color: "gray"
337
+ }
338
+ ], bodyLines: props.stats.dayUsage.flatMap((row) => {
339
+ if (row.totals.tokenBreakdown.schema !== "anthropic") {
340
+ return [];
341
+ }
342
+ return [
343
+ {
344
+ key: `day-row:${row.dayKey}`,
345
+ text: `${pad(formatUtcDay(row.dayKey), ANTHROPIC_DAY_USAGE_COLUMNS.day)} ${pad(formatInteger(row.totals.eventCount), ANTHROPIC_DAY_USAGE_COLUMNS.events)} ${pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(row.totals.outputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.output)} ${pad(formatUsageUsd(row.totals), ANTHROPIC_DAY_USAGE_COLUMNS.value)}`,
346
+ inverse: props.selectedDayKey === row.dayKey,
347
+ color: props.selectedDayKey === row.dayKey ? "cyan" : undefined
348
+ }
349
+ ];
350
+ }), selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
351
+ }
352
+ return (_jsx(ScrollableLineViewport, { headerLines: [
353
+ {
354
+ key: "openai-day-header",
355
+ text: `${pad("day", OPENAI_DAY_USAGE_COLUMNS.day)} ${pad("events", OPENAI_DAY_USAGE_COLUMNS.events)} ${pad("input", OPENAI_DAY_USAGE_COLUMNS.input)} ${pad("output", OPENAI_DAY_USAGE_COLUMNS.output)} value`,
356
+ color: "gray"
357
+ }
358
+ ], bodyLines: props.stats.dayUsage.flatMap((row) => {
359
+ if (row.totals.tokenBreakdown.schema !== "openai") {
360
+ return [];
361
+ }
362
+ return [
363
+ {
364
+ key: `day-row:${row.dayKey}`,
365
+ text: `${pad(formatUtcDay(row.dayKey), OPENAI_DAY_USAGE_COLUMNS.day)} ${pad(formatInteger(row.totals.eventCount), OPENAI_DAY_USAGE_COLUMNS.events)} ${pad(formatInteger(row.totals.inputTotalTokens), OPENAI_DAY_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.outputTokens), OPENAI_DAY_USAGE_COLUMNS.output)} ${pad(formatUsageUsd(row.totals), OPENAI_DAY_USAGE_COLUMNS.value)}`,
366
+ inverse: props.selectedDayKey === row.dayKey,
367
+ color: props.selectedDayKey === row.dayKey ? "cyan" : undefined
368
+ }
369
+ ];
370
+ }), selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
371
+ }
372
+ function ScrollableLineViewport(props) {
373
+ const headerLines = props.headerLines ?? [];
374
+ const footerLines = props.footerLines ?? [];
375
+ const layout = resolveScrollableViewportLayout(props.availableHeight, headerLines.length, props.bodyLines.length, footerLines.length);
376
+ const selectedBodyLineIndex = props.selectedBodyLineKey
377
+ ? props.bodyLines.findIndex((line) => line.key === props.selectedBodyLineKey)
378
+ : -1;
379
+ const scrollOffset = useAutoScrollOffset(selectedBodyLineIndex, props.bodyLines.length, layout.bodyVisibleCount);
380
+ const visibleHeaderLines = headerLines.slice(0, layout.headerVisibleCount);
381
+ const visibleBodyLines = props.bodyLines.slice(scrollOffset, scrollOffset + layout.bodyVisibleCount);
382
+ const visibleFooterLines = footerLines.slice(Math.max(0, footerLines.length - layout.footerVisibleCount));
383
+ const bodyScrollbarLines = buildScrollbarLines(layout.bodyVisibleCount, props.bodyLines.length, scrollOffset);
384
+ const showScrollbar = bodyScrollbarLines.length > 0;
385
+ return (_jsxs(Box, { flexDirection: "row", overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [visibleHeaderLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key))), visibleBodyLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key))), visibleFooterLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key)))] }), showScrollbar ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [visibleHeaderLines.map((line) => (_jsx(Text, { color: "gray", children: " " }, `${line.key}-scrollbar`))), bodyScrollbarLines.map((line, index) => (_jsx(Text, { color: line === "#" ? "cyan" : "gray", children: line }, `scrollbar-${index}`))), visibleFooterLines.map((line) => (_jsx(Text, { color: "gray", children: " " }, `${line.key}-scrollbar`)))] })) : null] }));
386
+ }
387
+ function ScrollableViewportLine(props) {
388
+ return (_jsx(Text, { bold: props.line.bold, color: props.line.color, inverse: props.line.inverse, wrap: "truncate-end", children: props.line.text }));
389
+ }
390
+ function buildLimitWindowSectionLines(scope, windows, selectedRowKey) {
391
+ if (windows.length === 0) {
392
+ return [{ key: `${scope}-empty`, text: "No windows found.", color: "gray" }];
393
+ }
394
+ return [
395
+ {
396
+ key: `${scope}-header`,
397
+ text: `${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`,
398
+ color: "gray"
399
+ },
400
+ ...windows.map((window) => {
401
+ const lineKey = getLimitRowKey(window);
402
+ const windowLabel = formatWindowMinutes(window.windowMinutes);
403
+ const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
404
+ const isSelected = selectedRowKey === lineKey;
405
+ return {
406
+ key: `limit-row:${lineKey}`,
407
+ text: `${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)}`,
408
+ inverse: isSelected,
409
+ color: isSelected ? "cyan" : undefined
410
+ };
411
+ })
412
+ ];
266
413
  }
267
414
  function SelectionDetailsPanel(props) {
268
415
  if (props.providerState.status !== "ready") {
@@ -274,17 +421,16 @@ function SelectionDetailsPanel(props) {
274
421
  }
275
422
  if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
276
423
  const row = props.selectedDayRow;
277
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Day details" }), _jsxs(Text, { children: ["day: ", formatUtcDay(row.dayKey), " events: ", formatInteger(row.totals.eventCount), " models: ", formatInteger(row.distinctModels.length), " plans: ", formatInteger(row.distinctPlanTypes.length)] }), _jsxs(Text, { children: ["range: ", formatEventRange(row.firstEventUtcIso, row.lastEventUtcIso)] }), _jsxs(Text, { children: ["input: ", formatInteger(row.totals.inputTokens), " cached: ", formatCacheTokens(row.totals, "cached")] }), _jsxs(Text, { children: ["non-cached: ", formatCacheTokens(row.totals, "non-cached"), " output: ", formatInteger(row.totals.outputTokens)] }), _jsxs(Text, { children: ["models: ", row.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", row.distinctPlanTypes.join(", ") || "none"] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
424
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Day details" }), _jsxs(Text, { children: ["day: ", formatUtcDay(row.dayKey), " events: ", formatInteger(row.totals.eventCount), " models: ", formatInteger(row.distinctModels.length), " plans: ", formatInteger(row.distinctPlanTypes.length)] }), _jsxs(Text, { children: ["range: ", formatEventRange(row.firstEventUtcIso, row.lastEventUtcIso)] }), _jsxs(Text, { children: ["models: ", row.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", row.distinctPlanTypes.join(", ") || "none"] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
278
425
  }
279
426
  if (props.tabId === "usage-by-model" && props.selectedModelRow) {
280
- 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 })] }));
427
+ 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, modelId: props.selectedModelRow.modelId })] }));
281
428
  }
282
429
  return null;
283
430
  }
284
431
  function UsageTotalsDetails(props) {
285
432
  const { totals } = props;
286
- const inputPerOutput = formatInputPerOutput(totals);
287
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Total credits burned: ", formatUsageCredits(totals)] }), _jsxs(Text, { children: ["Credits Value (@ $0.01/credit): ", formatUsageUsd(totals)] }), _jsxs(Text, { children: ["IpO: ", inputPerOutput.cached, ":", inputPerOutput.nonCached, ":", inputPerOutput.output] })] }));
433
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(UsageBreakdownLines, { totals: totals }), _jsxs(Text, { children: ["Burned tokens for: ", formatUsageUsd(totals, props.modelId)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(totals)] })] }));
288
434
  }
289
435
  function formatInteger(value) {
290
436
  return Math.round(value).toLocaleString("en-US");
@@ -298,19 +444,22 @@ function formatCredits(value) {
298
444
  maximumFractionDigits: 2
299
445
  });
300
446
  }
301
- function formatUsageCredits(totals) {
447
+ function formatUsageCredits(totals, modelId) {
448
+ if (isInternalUsageModel(modelId)) {
449
+ return "N/A";
450
+ }
302
451
  return totals.estimatedCreditsStatus === "unavailable" ? "unknown" : formatCredits(totals.estimatedCredits);
303
452
  }
304
- function formatUsageUsd(totals) {
453
+ function formatUsageUsd(totals, modelId) {
454
+ if (isInternalUsageModel(modelId)) {
455
+ return "N/A";
456
+ }
305
457
  return totals.estimatedCreditsStatus === "unavailable"
306
458
  ? "unknown"
307
459
  : formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD);
308
460
  }
309
- function formatCacheTokens(totals, kind) {
310
- if (totals.cacheStatus === "unavailable") {
311
- return "unknown";
312
- }
313
- return formatInteger(kind === "cached" ? totals.cachedInputTokens : totals.nonCachedInputTokens);
461
+ function isInternalUsageModel(modelId) {
462
+ return modelId === "codex-auto-review" || modelId === "<synthetic>";
314
463
  }
315
464
  function formatUsd(value) {
316
465
  if (value > 0 && value < 0.0001) {
@@ -365,19 +514,107 @@ function formatEventRange(firstEventUtcIso, lastEventUtcIso) {
365
514
  function pad(value, length) {
366
515
  return value.length >= length ? value.slice(0, length) : value.padEnd(length);
367
516
  }
368
- function formatInputPerOutput(totals) {
369
- if (totals.cacheStatus === "unavailable") {
370
- return { cached: "unknown", nonCached: "unknown", output: "1" };
517
+ function UsageBreakdownLines(props) {
518
+ const { totals } = props;
519
+ if (totals.tokenBreakdown.schema === "anthropic") {
520
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input total: ", formatInteger(totals.inputTotalTokens), " input: ", formatInteger(totals.tokenBreakdown.inputTokens), " cacheW5m: ", formatInteger(totals.tokenBreakdown.cacheWrite5mInputTokens)] }), _jsxs(Text, { children: ["cacheW1h: ", formatInteger(totals.tokenBreakdown.cacheWrite1hInputTokens), " cacheRead: ", formatInteger(totals.tokenBreakdown.cacheReadInputTokens), " output: ", formatInteger(totals.outputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
521
+ }
522
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input total: ", formatInteger(totals.inputTotalTokens), " uncached: ", formatOpenAiTokens(totals, "non-cached"), " cached: ", formatOpenAiTokens(totals, "cached")] }), _jsxs(Text, { children: ["output: ", formatInteger(totals.outputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
523
+ }
524
+ function formatOpenAiTokens(totals, kind) {
525
+ if (totals.tokenBreakdown.schema !== "openai" || totals.cacheStatus === "unavailable") {
526
+ return "unknown";
371
527
  }
528
+ return formatInteger(kind === "non-cached" ? totals.tokenBreakdown.nonCachedInputTokens : totals.tokenBreakdown.cachedInputTokens);
529
+ }
530
+ function formatInputPerOutput(totals) {
372
531
  if (totals.outputTokens <= 0) {
373
- return { cached: "0", nonCached: "0", output: "0" };
532
+ return totals.tokenBreakdown.schema === "anthropic" ? "input:cacheW5m:cacheW1h:cacheRead:output = 0:0:0:0:0" : "uncached:cached:output = 0:0:0";
533
+ }
534
+ if (totals.tokenBreakdown.schema === "anthropic") {
535
+ return `input:cacheW5m:cacheW1h:cacheRead:output = ${formatInteger(Math.round(totals.tokenBreakdown.inputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheWrite5mInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheWrite1hInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheReadInputTokens / totals.outputTokens))}:1`;
536
+ }
537
+ if (totals.cacheStatus === "unavailable") {
538
+ return "uncached:cached:output = unknown:unknown:1";
374
539
  }
540
+ return `uncached:cached:output = ${formatInteger(Math.round(totals.tokenBreakdown.nonCachedInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cachedInputTokens / totals.outputTokens))}:1`;
541
+ }
542
+ function useMeasuredElementSize() {
543
+ const ref = useRef(null);
544
+ const [size, setSize] = useState({ width: 0, height: 0 });
545
+ useEffect(() => {
546
+ if (!ref.current) {
547
+ return;
548
+ }
549
+ const nextSize = measureElement(ref.current);
550
+ setSize((current) => current.width === nextSize.width && current.height === nextSize.height ? current : nextSize);
551
+ });
375
552
  return {
376
- cached: formatInteger(Math.round(totals.cachedInputTokens / totals.outputTokens)),
377
- nonCached: formatInteger(Math.round(totals.nonCachedInputTokens / totals.outputTokens)),
378
- output: "1"
553
+ ref,
554
+ width: size.width,
555
+ height: size.height
379
556
  };
380
557
  }
558
+ function useAutoScrollOffset(selectedIndex, rowCount, viewportSize) {
559
+ const [scrollOffset, setScrollOffset] = useState(0);
560
+ useEffect(() => {
561
+ const maxOffset = Math.max(0, rowCount - Math.max(0, viewportSize));
562
+ setScrollOffset((current) => {
563
+ let next = Math.max(0, Math.min(current, maxOffset));
564
+ if (selectedIndex < 0 || viewportSize <= 0) {
565
+ return next;
566
+ }
567
+ if (selectedIndex < next) {
568
+ next = selectedIndex;
569
+ }
570
+ else if (selectedIndex >= next + viewportSize) {
571
+ next = selectedIndex - viewportSize + 1;
572
+ }
573
+ return Math.max(0, Math.min(next, maxOffset));
574
+ });
575
+ }, [rowCount, selectedIndex, viewportSize]);
576
+ return scrollOffset;
577
+ }
578
+ function resolveScrollableViewportLayout(availableHeight, headerCount, bodyCount, footerCount) {
579
+ const totalHeight = Math.max(1, availableHeight || 1);
580
+ if (bodyCount === 0) {
581
+ const headerVisibleCount = Math.min(headerCount, totalHeight);
582
+ return {
583
+ headerVisibleCount,
584
+ bodyVisibleCount: 0,
585
+ footerVisibleCount: Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount))
586
+ };
587
+ }
588
+ let headerVisibleCount = Math.min(headerCount, totalHeight);
589
+ let footerVisibleCount = Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount - 1));
590
+ let bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
591
+ if (bodyVisibleCount === 0 && footerVisibleCount > 0) {
592
+ footerVisibleCount -= 1;
593
+ bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
594
+ }
595
+ if (bodyVisibleCount === 0 && headerVisibleCount > 0) {
596
+ headerVisibleCount -= 1;
597
+ bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
598
+ }
599
+ if (bodyVisibleCount === 0) {
600
+ return {
601
+ headerVisibleCount: 0,
602
+ bodyVisibleCount: Math.min(bodyCount, totalHeight),
603
+ footerVisibleCount: 0
604
+ };
605
+ }
606
+ return { headerVisibleCount, bodyVisibleCount, footerVisibleCount };
607
+ }
608
+ function buildScrollbarLines(viewportSize, rowCount, scrollOffset) {
609
+ if (viewportSize <= 0 || rowCount <= viewportSize) {
610
+ return [];
611
+ }
612
+ const thumbSize = Math.max(1, Math.round((viewportSize * viewportSize) / rowCount));
613
+ const maxThumbOffset = Math.max(0, viewportSize - thumbSize);
614
+ const maxScrollOffset = Math.max(1, rowCount - viewportSize);
615
+ const thumbOffset = Math.round((Math.max(0, Math.min(scrollOffset, maxScrollOffset)) / maxScrollOffset) * maxThumbOffset);
616
+ return Array.from({ length: viewportSize }, (_, index) => index >= thumbOffset && index < thumbOffset + thumbSize ? SCROLLBAR_THUMB_GLYPH : SCROLLBAR_TRACK_GLYPH);
617
+ }
381
618
  function clampSelectionIndex(value, rowCount) {
382
619
  if (rowCount === 0) {
383
620
  return -1;
@@ -396,7 +633,7 @@ function providerUsageScore(state) {
396
633
  return 0;
397
634
  }
398
635
  const totals = state.stats.summary.totals;
399
- return totals.inputTokens + totals.cachedInputTokens + totals.outputTokens;
636
+ return totals.inputTotalTokens + totals.outputTokens;
400
637
  }
401
638
  function getLimitRows(providerState) {
402
639
  if (providerState.status !== "ready") {
@@ -424,7 +661,59 @@ function parseStatsOptions(argv) {
424
661
  verbose: argv.includes("-v") || argv.includes("--verbose")
425
662
  };
426
663
  }
664
+ function useViewportHeight() {
665
+ const { stdout } = useStdout();
666
+ const [viewportHeight, setViewportHeight] = useState(() => resolveViewportHeight(stdout.rows));
667
+ useEffect(() => {
668
+ const updateViewportHeight = () => {
669
+ setViewportHeight(resolveViewportHeight(stdout.rows));
670
+ };
671
+ updateViewportHeight();
672
+ if (!stdout.isTTY) {
673
+ return;
674
+ }
675
+ stdout.on("resize", updateViewportHeight);
676
+ return () => {
677
+ stdout.off("resize", updateViewportHeight);
678
+ };
679
+ }, [stdout]);
680
+ return viewportHeight;
681
+ }
682
+ function resolveViewportHeight(rows) {
683
+ const terminalRows = typeof rows === "number" && rows > 0 ? rows : 24;
684
+ // Keep Ink below the terminal height so it stays on its incremental redraw path
685
+ // instead of the full-screen print path that can leave the viewport scrolled.
686
+ return Math.max(1, terminalRows - 1);
687
+ }
427
688
  export function main(argv = process.argv.slice(2)) {
428
- render(_jsx(App, { statsOptions: parseStatsOptions(argv) }));
689
+ const restoreFullscreen = enterFullscreenMode(process.stdout);
690
+ const exitHandler = () => {
691
+ restoreFullscreen();
692
+ };
693
+ process.once("exit", exitHandler);
694
+ const instance = render(_jsx(App, { statsOptions: parseStatsOptions(argv) }), {
695
+ stdout: process.stdout,
696
+ stdin: process.stdin,
697
+ stderr: process.stderr
698
+ });
699
+ void instance.waitUntilExit().finally(() => {
700
+ process.off("exit", exitHandler);
701
+ instance.cleanup();
702
+ restoreFullscreen();
703
+ });
704
+ }
705
+ function enterFullscreenMode(stdout) {
706
+ if (!stdout.isTTY) {
707
+ return () => { };
708
+ }
709
+ let restored = false;
710
+ stdout.write(ENTER_FULLSCREEN_MODE);
711
+ return () => {
712
+ if (restored) {
713
+ return;
714
+ }
715
+ restored = true;
716
+ stdout.write(EXIT_FULLSCREEN_MODE);
717
+ };
429
718
  }
430
719
  main();
@@ -73,7 +73,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
73
73
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
74
74
  const unknownPricedModels = modelUsage
75
75
  .map((row) => row.modelId)
76
- .filter((modelId) => !resolveRate(modelId));
76
+ .filter((modelId) => !resolveRate(modelId) && !isInternalClaudeModel(modelId));
77
77
  if (unknownPricedModels.length > 0) {
78
78
  warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
79
79
  }
@@ -135,43 +135,61 @@ function resolveRate(modelId) {
135
135
  }
136
136
  return undefined;
137
137
  }
138
+ function isInternalClaudeModel(modelId) {
139
+ return modelId === "<synthetic>";
140
+ }
138
141
  function creditsFor(modelId, usage) {
139
142
  const rate = resolveRate(modelId);
140
143
  if (!rate) {
141
144
  return 0;
142
145
  }
143
- const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
144
- const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
146
+ const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
145
147
  const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
146
148
  return (((usage.inputTokens / 1000000) * rate.input +
147
149
  (usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
148
- (usage.cacheCreation5mInputTokens / 1000000) * rate.cacheWrite5m +
149
- (usage.cacheCreation1hInputTokens / 1000000) * rate.cacheWrite1h +
150
- (cacheWriteFallbackTokens / 1000000) * rate.cacheWrite5m +
150
+ (cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
151
+ (cacheWriteBreakdown.cacheWrite1hInputTokens / 1000000) * rate.cacheWrite1h +
151
152
  (usage.outputTokens / 1000000) * rate.output) *
152
153
  inferenceMultiplier *
153
154
  USD_TO_CREDITS);
154
155
  }
155
156
  function usageToTotals(modelId, usage) {
156
- const nonCachedInputTokens = usage.inputTokens + usage.cacheCreationInputTokens;
157
- const cachedInputTokens = usage.cacheReadInputTokens;
157
+ const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
158
+ const inputTotalTokens = usage.inputTokens +
159
+ cacheWriteBreakdown.cacheWrite5mInputTokens +
160
+ cacheWriteBreakdown.cacheWrite1hInputTokens +
161
+ usage.cacheReadInputTokens;
158
162
  return {
159
- inputTokens: nonCachedInputTokens + cachedInputTokens,
160
- cachedInputTokens,
161
- nonCachedInputTokens,
163
+ inputTotalTokens,
162
164
  outputTokens: usage.outputTokens,
163
165
  reasoningOutputTokens: 0,
164
- totalTokens: nonCachedInputTokens + cachedInputTokens + usage.outputTokens,
166
+ totalTokens: inputTotalTokens + usage.outputTokens,
165
167
  estimatedCredits: creditsFor(modelId, usage),
166
- eventCount: 1
168
+ eventCount: 1,
169
+ tokenBreakdown: {
170
+ schema: "anthropic",
171
+ inputTokens: usage.inputTokens,
172
+ cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
173
+ cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
174
+ cacheReadInputTokens: usage.cacheReadInputTokens,
175
+ outputTokens: usage.outputTokens
176
+ }
167
177
  };
168
178
  }
169
179
  function addModelUsage(byModel, modelId, deltaTotals) {
170
180
  const resolvedModelId = modelId || "unknown";
171
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
181
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("anthropic");
172
182
  addUsageTotals(totals, deltaTotals);
173
183
  byModel.set(resolvedModelId, totals);
174
184
  }
185
+ function resolveClaudeCacheWriteBreakdown(usage) {
186
+ const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
187
+ const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
188
+ return {
189
+ cacheWrite5mInputTokens: usage.cacheCreation5mInputTokens + cacheWriteFallbackTokens,
190
+ cacheWrite1hInputTokens: usage.cacheCreation1hInputTokens
191
+ };
192
+ }
175
193
  function isSessionFile(filePath) {
176
194
  return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
177
195
  }
@@ -17,6 +17,7 @@ export class CodexUsageProvider extends UsageProviderBase {
17
17
  }
18
18
  async getStats(_options = {}) {
19
19
  const sessionsRoot = path.join(this.root, ".codex", "sessions");
20
+ const knownModels = await readCodexModelMetadata(this.root);
20
21
  const byModel = new Map();
21
22
  const byDay = createDailyUsageAggregates();
22
23
  const windows = createLimitWindowAggregates();
@@ -30,7 +31,7 @@ export class CodexUsageProvider extends UsageProviderBase {
30
31
  };
31
32
  for await (const file of walkSessionFiles(sessionsRoot)) {
32
33
  parseTotals.filesScanned += 1;
33
- const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes);
34
+ const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes, knownModels);
34
35
  parseTotals.linesRead += fileStats.linesRead;
35
36
  parseTotals.tokenEvents += fileStats.tokenEvents;
36
37
  parseTotals.malformedLines += fileStats.malformedLines;
@@ -43,7 +44,7 @@ export class CodexUsageProvider extends UsageProviderBase {
43
44
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
44
45
  const unknownPricedModels = modelUsage
45
46
  .map((row) => row.modelId)
46
- .filter((modelId) => !RATE_CARD[modelId]);
47
+ .filter((modelId) => !RATE_CARD[modelId] && !isAssumedZeroRatedCodexModel(modelId, knownModels));
47
48
  if (unknownPricedModels.length > 0) {
48
49
  warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
49
50
  }
@@ -74,6 +75,39 @@ export class CodexUsageProvider extends UsageProviderBase {
74
75
  };
75
76
  }
76
77
  }
78
+ async function readCodexModelMetadata(root) {
79
+ const modelsCachePath = path.join(root, ".codex", "models_cache.json");
80
+ let fileText;
81
+ try {
82
+ fileText = await fs.promises.readFile(modelsCachePath, "utf8");
83
+ }
84
+ catch {
85
+ return new Map();
86
+ }
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(fileText);
90
+ }
91
+ catch {
92
+ return new Map();
93
+ }
94
+ const models = asRecord(payload)?.models;
95
+ if (!Array.isArray(models)) {
96
+ return new Map();
97
+ }
98
+ const metadata = new Map();
99
+ for (const model of models) {
100
+ const record = asRecord(model);
101
+ const slug = typeof record?.slug === "string" ? record.slug : "";
102
+ if (!slug) {
103
+ continue;
104
+ }
105
+ metadata.set(slug, {
106
+ visibility: typeof record?.visibility === "string" ? record.visibility : ""
107
+ });
108
+ }
109
+ return metadata;
110
+ }
77
111
  function createEmptyRawUsage() {
78
112
  return {
79
113
  inputTokens: 0,
@@ -107,38 +141,51 @@ function creditsFor(modelId, usage) {
107
141
  if (!rate) {
108
142
  return 0;
109
143
  }
110
- const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
111
- const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
112
- return ((nonCachedInputTokens / 1000000) * rate.input +
113
- (cachedInputTokens / 1000000) * rate.cachedInput +
144
+ return ((usage.inputTokens / 1000000) * rate.input +
145
+ (usage.cachedInputTokens / 1000000) * rate.cachedInput +
114
146
  (usage.outputTokens / 1000000) * rate.output);
115
147
  }
116
148
  function rawUsageToTotals(usage) {
117
- const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
149
+ const inputTotalTokens = usage.inputTokens + usage.cachedInputTokens;
118
150
  return {
119
- inputTokens: usage.inputTokens,
120
- cachedInputTokens,
121
- nonCachedInputTokens: Math.max(0, usage.inputTokens - cachedInputTokens),
151
+ inputTotalTokens,
122
152
  outputTokens: usage.outputTokens,
123
153
  reasoningOutputTokens: usage.reasoningOutputTokens,
124
- totalTokens: usage.totalTokens,
154
+ totalTokens: inputTotalTokens + usage.outputTokens,
125
155
  estimatedCredits: 0,
126
- eventCount: 0
156
+ eventCount: 0,
157
+ tokenBreakdown: {
158
+ schema: "openai",
159
+ nonCachedInputTokens: usage.inputTokens,
160
+ cachedInputTokens: usage.cachedInputTokens,
161
+ outputTokens: usage.outputTokens
162
+ }
127
163
  };
128
164
  }
129
- function createUsageTotalsForModel(modelId, usage) {
165
+ function createUsageTotalsForModel(modelId, usage, knownModels) {
130
166
  const resolvedModelId = modelId || "unknown";
131
167
  const deltaTotals = rawUsageToTotals(usage);
132
168
  deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
133
169
  deltaTotals.eventCount = 1;
170
+ if (!RATE_CARD[resolvedModelId] && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
171
+ deltaTotals.estimatedCreditsStatus = "unavailable";
172
+ }
134
173
  return deltaTotals;
135
174
  }
136
175
  function addModelUsage(byModel, modelId, deltaTotals) {
137
176
  const resolvedModelId = modelId || "unknown";
138
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
177
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("openai");
139
178
  addUsageTotals(totals, deltaTotals);
140
179
  byModel.set(resolvedModelId, totals);
141
180
  }
181
+ function isHiddenCodexModel(modelId, knownModels) {
182
+ return knownModels.get(modelId)?.visibility === "hide";
183
+ }
184
+ function isAssumedZeroRatedCodexModel(modelId, knownModels) {
185
+ // Hidden internal Codex models do not have a public rate card entry. For dashboard
186
+ // rollups we treat them as zero-rated so they do not turn aggregate totals unknown.
187
+ return isHiddenCodexModel(modelId, knownModels);
188
+ }
142
189
  function isSessionFile(filePath) {
143
190
  return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
144
191
  }
@@ -160,7 +207,7 @@ async function* walkSessionFiles(directory) {
160
207
  }
161
208
  }
162
209
  }
163
- async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
210
+ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, knownModels) {
164
211
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
165
212
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
166
213
  let currentModel = "unknown";
@@ -201,7 +248,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
201
248
  const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
202
249
  previousTotal = totalUsage;
203
250
  const resolvedModelId = currentModel || "unknown";
204
- const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage);
251
+ const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage, knownModels);
205
252
  tokenEvents += 1;
206
253
  addModelUsage(byModel, resolvedModelId, deltaTotals);
207
254
  const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
@@ -4,27 +4,31 @@ export class UsageProviderBase {
4
4
  this.label = label;
5
5
  }
6
6
  }
7
- export function createEmptyUsageTotals() {
7
+ export function createEmptyUsageTotals(schema = "openai") {
8
8
  return {
9
- inputTokens: 0,
10
- cachedInputTokens: 0,
11
- nonCachedInputTokens: 0,
9
+ inputTotalTokens: 0,
12
10
  outputTokens: 0,
13
11
  reasoningOutputTokens: 0,
14
12
  totalTokens: 0,
15
13
  estimatedCredits: 0,
16
- eventCount: 0
14
+ eventCount: 0,
15
+ tokenBreakdown: createEmptyUsageTokenBreakdown(schema)
16
+ };
17
+ }
18
+ export function cloneUsageTotals(totals) {
19
+ return {
20
+ ...totals,
21
+ tokenBreakdown: { ...totals.tokenBreakdown }
17
22
  };
18
23
  }
19
24
  export function addUsageTotals(target, source) {
20
- target.inputTokens += source.inputTokens;
21
- target.cachedInputTokens += source.cachedInputTokens;
22
- target.nonCachedInputTokens += source.nonCachedInputTokens;
25
+ target.inputTotalTokens += source.inputTotalTokens;
23
26
  target.outputTokens += source.outputTokens;
24
27
  target.reasoningOutputTokens += source.reasoningOutputTokens;
25
28
  target.totalTokens += source.totalTokens;
26
29
  target.estimatedCredits += source.estimatedCredits;
27
30
  target.eventCount += source.eventCount;
31
+ addUsageTokenBreakdown(target.tokenBreakdown, source.tokenBreakdown);
28
32
  if (source.cacheStatus === "unavailable") {
29
33
  target.cacheStatus = "unavailable";
30
34
  }
@@ -33,9 +37,44 @@ export function addUsageTotals(target, source) {
33
37
  }
34
38
  }
35
39
  export function sumUsageTotals(rows) {
36
- const totals = createEmptyUsageTotals();
40
+ const totals = createEmptyUsageTotals(rows[0]?.tokenBreakdown.schema ?? "openai");
37
41
  for (const row of rows) {
38
42
  addUsageTotals(totals, row);
39
43
  }
40
44
  return totals;
41
45
  }
46
+ function createEmptyUsageTokenBreakdown(schema) {
47
+ if (schema === "anthropic") {
48
+ return {
49
+ schema,
50
+ inputTokens: 0,
51
+ cacheWrite5mInputTokens: 0,
52
+ cacheWrite1hInputTokens: 0,
53
+ cacheReadInputTokens: 0,
54
+ outputTokens: 0
55
+ };
56
+ }
57
+ return {
58
+ schema,
59
+ nonCachedInputTokens: 0,
60
+ cachedInputTokens: 0,
61
+ outputTokens: 0
62
+ };
63
+ }
64
+ function addUsageTokenBreakdown(target, source) {
65
+ if (target.schema !== source.schema) {
66
+ throw new Error(`Cannot merge ${source.schema} usage totals into ${target.schema} totals.`);
67
+ }
68
+ target.outputTokens += source.outputTokens;
69
+ if (target.schema === "anthropic" && source.schema === "anthropic") {
70
+ target.inputTokens += source.inputTokens;
71
+ target.cacheWrite5mInputTokens += source.cacheWrite5mInputTokens;
72
+ target.cacheWrite1hInputTokens += source.cacheWrite1hInputTokens;
73
+ target.cacheReadInputTokens += source.cacheReadInputTokens;
74
+ return;
75
+ }
76
+ if (target.schema === "openai" && source.schema === "openai") {
77
+ target.nonCachedInputTokens += source.nonCachedInputTokens;
78
+ target.cachedInputTokens += source.cachedInputTokens;
79
+ }
80
+ }
@@ -222,16 +222,24 @@ function isCopilotChatSpan(attributes) {
222
222
  function createUsageTotals(modelId, usage) {
223
223
  const hasCacheInfo = usage.cachedInputTokens !== undefined || usage.cacheCreationInputTokens !== undefined;
224
224
  const hasKnownCreditPricing = isNonBillableModel(modelId) || (hasCacheInfo && rateForModel(modelId, usage.inputTokens) !== undefined);
225
- const cachedInputTokens = hasCacheInfo ? Math.min(usage.cachedInputTokens ?? 0, usage.inputTokens) : 0;
225
+ const cachedInputTokens = hasCacheInfo ? Math.max(0, usage.cachedInputTokens ?? 0) : 0;
226
+ const cacheWriteInputTokens = hasCacheInfo ? Math.max(0, usage.cacheCreationInputTokens ?? 0) : 0;
227
+ const uncachedInputTokens = hasCacheInfo
228
+ ? Math.max(0, usage.inputTokens - cachedInputTokens - cacheWriteInputTokens)
229
+ : 0;
226
230
  return {
227
- inputTokens: usage.inputTokens,
228
- cachedInputTokens,
229
- nonCachedInputTokens: hasCacheInfo ? Math.max(0, usage.inputTokens - cachedInputTokens) : 0,
231
+ inputTotalTokens: usage.inputTokens,
230
232
  outputTokens: usage.outputTokens,
231
233
  reasoningOutputTokens: Math.min(usage.reasoningOutputTokens ?? 0, usage.outputTokens),
232
234
  totalTokens: usage.inputTokens + usage.outputTokens,
233
235
  estimatedCredits: creditsFor(modelId, usage),
234
236
  eventCount: 1,
237
+ tokenBreakdown: {
238
+ schema: "openai",
239
+ nonCachedInputTokens: uncachedInputTokens,
240
+ cachedInputTokens,
241
+ outputTokens: usage.outputTokens
242
+ },
235
243
  cacheStatus: hasCacheInfo ? "known" : "unavailable",
236
244
  estimatedCreditsStatus: hasKnownCreditPricing ? "known" : "unavailable"
237
245
  };
@@ -272,7 +280,7 @@ function isNonBillableModel(modelId) {
272
280
  }
273
281
  function addModelUsage(byModel, modelId, deltaTotals) {
274
282
  const resolvedModelId = modelId || "unknown";
275
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
283
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("openai");
276
284
  addUsageTotals(totals, deltaTotals);
277
285
  byModel.set(resolvedModelId, totals);
278
286
  }
@@ -1,4 +1,4 @@
1
- import { addUsageTotals } from "./contract.js";
1
+ import { addUsageTotals, cloneUsageTotals } from "./contract.js";
2
2
  export function createDailyUsageAggregates() {
3
3
  return new Map();
4
4
  }
@@ -18,7 +18,7 @@ export function addDailyUsage(rows, eventTimeMs, modelId, planType, deltaTotals)
18
18
  sortTimeMs,
19
19
  firstEventMs: Number.isFinite(eventTimeMs) ? eventTimeMs : null,
20
20
  lastEventMs: Number.isFinite(eventTimeMs) ? eventTimeMs : null,
21
- totals: { ...deltaTotals },
21
+ totals: cloneUsageTotals(deltaTotals),
22
22
  models,
23
23
  planTypes
24
24
  });
@@ -46,7 +46,7 @@ export function buildDailyUsageRows(rows) {
46
46
  lastEventUtcIso: row.lastEventMs === null ? null : formatIsoFromMilliseconds(row.lastEventMs),
47
47
  distinctModels: [...row.models].sort(),
48
48
  distinctPlanTypes: [...row.planTypes].sort(),
49
- totals: { ...row.totals }
49
+ totals: cloneUsageTotals(row.totals)
50
50
  }));
51
51
  }
52
52
  function resolveDayBucket(eventTimeMs) {
@@ -1,4 +1,4 @@
1
- import { addUsageTotals, createEmptyUsageTotals } from "./contract.js";
1
+ import { addUsageTotals, cloneUsageTotals, createEmptyUsageTotals } from "./contract.js";
2
2
  export function createLimitWindowAggregates() {
3
3
  return new Map();
4
4
  }
@@ -71,7 +71,7 @@ function collapseNearbyWindows(rows) {
71
71
  if (!existing) {
72
72
  collapsed.set(key, {
73
73
  ...row,
74
- totals: { ...row.totals }
74
+ totals: cloneUsageTotals(row.totals)
75
75
  });
76
76
  continue;
77
77
  }
@@ -93,7 +93,7 @@ function collapseNearbyWindows(rows) {
93
93
  function computeWindowTotals(events) {
94
94
  // Session files are not guaranteed to be parsed in timestamp order, so
95
95
  // saturation has to be applied after we sort the captured window events.
96
- const totals = createEmptyUsageTotals();
96
+ const totals = createEmptyUsageTotals(events[0]?.totals.tokenBreakdown.schema ?? "openai");
97
97
  let sawBelowCap = false;
98
98
  let isExhausted = false;
99
99
  for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
@@ -132,7 +132,7 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
132
132
  lastSeenMs: eventTimeMs,
133
133
  minUsedPercent: usedPercent,
134
134
  maxUsedPercent: usedPercent,
135
- events: [{ eventTimeMs, usedPercent, totals: { ...deltaTotals } }]
135
+ events: [{ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
136
136
  });
137
137
  return;
138
138
  }
@@ -142,5 +142,5 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
142
142
  existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
143
143
  existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
144
144
  existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
145
- existing.events.push({ eventTimeMs, usedPercent, totals: { ...deltaTotals } });
145
+ existing.events.push({ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) });
146
146
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": "./bin/letmecode.js",
9
10
  "files": [
@@ -18,6 +19,16 @@
18
19
  "publishConfig": {
19
20
  "access": "public"
20
21
  },
22
+ "scripts": {
23
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
24
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
25
+ "prepack": "npm run build",
26
+ "prestart": "npm run build",
27
+ "start": "node ./bin/letmecode.js",
28
+ "pretest": "npm run build",
29
+ "smoke": "node ./bin/letmecode.js",
30
+ "test": "node --test ink-app/test/*.test.mjs"
31
+ },
21
32
  "keywords": [
22
33
  "cli",
23
34
  "ink",
@@ -33,14 +44,5 @@
33
44
  "@types/node": "^24.0.7",
34
45
  "@types/react": "^18.3.24",
35
46
  "typescript": "^5.8.3"
36
- },
37
- "scripts": {
38
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
39
- "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
40
- "prestart": "npm run build",
41
- "start": "node ./bin/letmecode.js",
42
- "pretest": "npm run build",
43
- "smoke": "node ./bin/letmecode.js",
44
- "test": "node --test ink-app/test/*.test.mjs"
45
47
  }
46
- }
48
+ }