letmecode 0.1.23 → 0.1.25

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.
@@ -58,12 +58,12 @@ export function buildHelpText() {
58
58
  " --no-usage Disable anonymous usage reporting",
59
59
  "",
60
60
  "Controls:",
61
- " [ ] / Tab Switch providers",
62
- " Shift+Tab Switch providers backward",
63
- " j / k Switch dashboard sections",
64
- " Up / Down Switch dashboard sections",
65
- " Left / Right Select the previous or next row",
66
- " 1, h / l, Enter Run Copilot setup actions",
61
+ " Up / Down Select Provider, View, or table rows",
62
+ " Left / Right Change the selected Provider or View",
63
+ " Left / Right Do nothing when table rows are selected",
64
+ " Tab / Shift+Tab Switch providers without leaving the table",
65
+ " [ / ] Switch views without leaving the table",
66
+ " 1, Enter Run Copilot setup actions",
67
67
  " q or Esc Quit",
68
68
  "",
69
69
  "Trace logging:",
@@ -4,22 +4,24 @@ import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, rende
4
4
  import { buildHelpText, buildProviderStatsOptions, parseCliOptions } from "./cli-options.js";
5
5
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
6
6
  import { reportAnonymousUsage } from "./reporting.js";
7
+ import { estimateLimitFullValue } from "./providers/limits.js";
7
8
  const ESC = String.fromCharCode(0x1b);
8
9
  // Normal mouse tracking (button press/release only) + SGR extended coordinates.
9
- // This makes the tabs clickable while leaving Shift+drag native text selection
10
+ // This makes the controls clickable while leaving Shift+drag native text selection
10
11
  // available everywhere else, exactly like Midnight Commander.
11
12
  const ENABLE_MOUSE_TRACKING = `${ESC}[?1000h${ESC}[?1006h`;
12
13
  const DISABLE_MOUSE_TRACKING = `${ESC}[?1006l${ESC}[?1000l`;
13
14
  // SGR mouse report: ESC [ < button ; column ; row, ending in M (press) or m (release).
14
15
  const SGR_MOUSE_SEQUENCE = new RegExp(`${ESC}\\[<(\\d+);(\\d+);(\\d+)([Mm])`, "g");
15
- const DETAIL_TABS = [
16
+ const DETAIL_VIEWS = [
16
17
  { id: "limit-windows", label: "Limits" },
17
18
  { id: "summary", label: "Summary" },
18
19
  { id: "day-to-day-analyses", label: "Daily" },
19
20
  { id: "usage-by-model", label: "Models" }
20
21
  ];
22
+ const CONTROL_SECTIONS = ["provider", "view", "table"];
21
23
  const CODEX_CREDIT_COST_USD = 0.01;
22
- const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq."];
24
+ const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq.", "Full"];
23
25
  const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
24
26
  const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
25
27
  const COPILOT_ACTIONS = [
@@ -38,26 +40,29 @@ function App(props) {
38
40
  const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
39
41
  const [selectedProviderId, setSelectedProviderId] = useState(providers[0]?.id ?? "");
40
42
  const [hasUserSelectedProvider, setHasUserSelectedProvider] = useState(false);
41
- const [selectedDetailTabIndex, setSelectedDetailTabIndex] = useState(0);
42
- const [selectedLimitRowIndex, setSelectedLimitRowIndex] = useState(0);
43
- const [selectedDayRowIndex, setSelectedDayRowIndex] = useState(0);
44
- const [selectedModelRowIndex, setSelectedModelRowIndex] = useState(0);
43
+ const [selectedDetailViewIndex, setSelectedDetailViewIndex] = useState(0);
44
+ const [selectedControlSectionIndex, setSelectedControlSectionIndex] = useState(0);
45
+ const [selectedLimitRowIndex, setSelectedLimitRowIndex] = useState();
46
+ const [selectedDayRowIndex, setSelectedDayRowIndex] = useState();
47
+ const [selectedModelRowIndex, setSelectedModelRowIndex] = useState();
45
48
  const [selectedCopilotActionIndex, setSelectedCopilotActionIndex] = useState(0);
46
49
  const [copilotActionMessage, setCopilotActionMessage] = useState();
47
50
  const hasReportedAnonymousUsageRef = useRef(false);
48
51
  const sortedProviderStates = React.useMemo(() => sortProviderStatesByUsage(providerStates), [providerStates]);
49
52
  const selectedProviderIndex = Math.max(0, sortedProviderStates.findIndex((state) => state.provider.id === selectedProviderId));
50
53
  const selectedProvider = sortedProviderStates[selectedProviderIndex];
51
- const selectedDetailTab = DETAIL_TABS[selectedDetailTabIndex];
54
+ const selectedDetailView = DETAIL_VIEWS[selectedDetailViewIndex];
55
+ const selectedControlSection = CONTROL_SECTIONS[selectedControlSectionIndex];
56
+ const isTableControlSelected = selectedControlSection === "table";
52
57
  const limitRows = getLimitRows(selectedProvider);
53
58
  const dayRows = getDayRows(selectedProvider);
54
59
  const modelRows = getModelRows(selectedProvider);
55
- const activeLimitRowIndex = clampSelectionIndex(selectedLimitRowIndex, limitRows.length);
56
- const activeDayRowIndex = clampSelectionIndex(selectedDayRowIndex, dayRows.length);
57
- const activeModelRowIndex = clampSelectionIndex(selectedModelRowIndex, modelRows.length);
58
- const selectedLimitRow = activeLimitRowIndex >= 0 ? limitRows[activeLimitRowIndex] : undefined;
59
- const selectedDayRow = activeDayRowIndex >= 0 ? dayRows[activeDayRowIndex] : undefined;
60
- const selectedModelRow = activeModelRowIndex >= 0 ? modelRows[activeModelRowIndex] : undefined;
60
+ const activeLimitRowIndex = clampOptionalSelectionIndex(selectedLimitRowIndex, limitRows.length);
61
+ const activeDayRowIndex = clampOptionalSelectionIndex(selectedDayRowIndex, dayRows.length);
62
+ const activeModelRowIndex = clampOptionalSelectionIndex(selectedModelRowIndex, modelRows.length);
63
+ const selectedLimitRow = isTableControlSelected && activeLimitRowIndex >= 0 ? limitRows[activeLimitRowIndex] : undefined;
64
+ const selectedDayRow = isTableControlSelected && activeDayRowIndex >= 0 ? dayRows[activeDayRowIndex] : undefined;
65
+ const selectedModelRow = isTableControlSelected && activeModelRowIndex >= 0 ? modelRows[activeModelRowIndex] : undefined;
61
66
  useEffect(() => {
62
67
  let cancelled = false;
63
68
  for (const provider of providers) {
@@ -109,6 +114,11 @@ function App(props) {
109
114
  // Anonymous usage reporting is best-effort and must never disturb the TUI.
110
115
  });
111
116
  }, [props.usageReportingEnabled, providerStates]);
117
+ const clearSelectedTableRows = useCallback(() => {
118
+ setSelectedLimitRowIndex(undefined);
119
+ setSelectedDayRowIndex(undefined);
120
+ setSelectedModelRowIndex(undefined);
121
+ }, []);
112
122
  useMouseClick((click) => {
113
123
  const regionId = resolveClick(click);
114
124
  if (!regionId) {
@@ -117,23 +127,36 @@ function App(props) {
117
127
  if (regionId.startsWith("provider:")) {
118
128
  setSelectedProviderId(regionId.slice("provider:".length));
119
129
  setHasUserSelectedProvider(true);
130
+ setSelectedControlSectionIndex(CONTROL_SECTIONS.indexOf("provider"));
131
+ clearSelectedTableRows();
120
132
  return;
121
133
  }
122
- if (regionId.startsWith("vtab:")) {
123
- setSelectedDetailTabIndex(Number(regionId.slice("vtab:".length)));
134
+ if (regionId.startsWith("view:")) {
135
+ setSelectedDetailViewIndex(Number(regionId.slice("view:".length)));
136
+ setSelectedControlSectionIndex(CONTROL_SECTIONS.indexOf("view"));
137
+ clearSelectedTableRows();
124
138
  }
125
139
  });
126
140
  const moveSelectedTableRow = useCallback((delta) => {
127
- if (selectedDetailTab.id === "limit-windows") {
128
- setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex + delta, limitRows.length));
141
+ if (selectedDetailView.id === "limit-windows") {
142
+ if (limitRows.length === 0) {
143
+ return;
144
+ }
145
+ setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex < 0 ? 0 : activeLimitRowIndex + delta, limitRows.length));
129
146
  return;
130
147
  }
131
- if (selectedDetailTab.id === "usage-by-model") {
132
- setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex + delta, modelRows.length));
148
+ if (selectedDetailView.id === "usage-by-model") {
149
+ if (modelRows.length === 0) {
150
+ return;
151
+ }
152
+ setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex < 0 ? 0 : activeModelRowIndex + delta, modelRows.length));
133
153
  return;
134
154
  }
135
- if (selectedDetailTab.id === "day-to-day-analyses") {
136
- setSelectedDayRowIndex(clampSelectionIndex(activeDayRowIndex + delta, dayRows.length));
155
+ if (selectedDetailView.id === "day-to-day-analyses") {
156
+ if (dayRows.length === 0) {
157
+ return;
158
+ }
159
+ setSelectedDayRowIndex(clampSelectionIndex(activeDayRowIndex < 0 ? 0 : activeDayRowIndex + delta, dayRows.length));
137
160
  }
138
161
  }, [
139
162
  activeDayRowIndex,
@@ -142,8 +165,59 @@ function App(props) {
142
165
  dayRows.length,
143
166
  limitRows.length,
144
167
  modelRows.length,
145
- selectedDetailTab.id
168
+ selectedDetailView.id
146
169
  ]);
170
+ const canMoveTableRowUp = useCallback(() => {
171
+ if (selectedDetailView.id === "limit-windows") {
172
+ return activeLimitRowIndex > 0;
173
+ }
174
+ if (selectedDetailView.id === "usage-by-model") {
175
+ return activeModelRowIndex > 0;
176
+ }
177
+ if (selectedDetailView.id === "day-to-day-analyses") {
178
+ return activeDayRowIndex > 0;
179
+ }
180
+ return false;
181
+ }, [activeDayRowIndex, activeLimitRowIndex, activeModelRowIndex, selectedDetailView.id]);
182
+ const moveSelectionDown = useCallback(() => {
183
+ if (selectedControlSection === "table") {
184
+ moveSelectedTableRow(1);
185
+ return;
186
+ }
187
+ setSelectedControlSectionIndex((current) => {
188
+ const next = Math.min(current + 1, CONTROL_SECTIONS.length - 1);
189
+ if (CONTROL_SECTIONS[next] === "table") {
190
+ moveSelectedTableRow(1);
191
+ }
192
+ return next;
193
+ });
194
+ }, [moveSelectedTableRow, selectedControlSection]);
195
+ const moveSelectionUp = useCallback(() => {
196
+ if (selectedControlSection === "table" && canMoveTableRowUp()) {
197
+ moveSelectedTableRow(-1);
198
+ return;
199
+ }
200
+ if (selectedControlSection === "table") {
201
+ clearSelectedTableRows();
202
+ }
203
+ setSelectedControlSectionIndex((current) => Math.max(current - 1, 0));
204
+ }, [canMoveTableRowUp, clearSelectedTableRows, moveSelectedTableRow, selectedControlSection]);
205
+ const changeSelectedProvider = useCallback((delta) => {
206
+ setSelectedProviderId(sortedProviderStates[(selectedProviderIndex + delta + sortedProviderStates.length) % sortedProviderStates.length].provider.id);
207
+ setHasUserSelectedProvider(true);
208
+ }, [selectedProviderIndex, sortedProviderStates]);
209
+ const changeSelectedDetailView = useCallback((delta) => {
210
+ setSelectedDetailViewIndex((current) => (current + delta + DETAIL_VIEWS.length) % DETAIL_VIEWS.length);
211
+ }, []);
212
+ const changeSelectedSectionValue = useCallback((delta) => {
213
+ if (selectedControlSection === "provider") {
214
+ changeSelectedProvider(delta);
215
+ return;
216
+ }
217
+ if (selectedControlSection === "view") {
218
+ changeSelectedDetailView(delta);
219
+ }
220
+ }, [changeSelectedDetailView, changeSelectedProvider, selectedControlSection]);
147
221
  useInput((input, key) => {
148
222
  // Mouse reports arrive as SGR escape sequences and are handled by useMouseClick.
149
223
  // Ink strips the leading ESC, leaving e.g. "[<0;10;5M" — never treat that as a key.
@@ -159,45 +233,42 @@ function App(props) {
159
233
  return;
160
234
  }
161
235
  if (selectedProvider.provider.id === "copilot" && key.return) {
162
- runCopilotAction(COPILOT_ACTIONS[selectedCopilotActionIndex].id, setCopilotActionMessage);
236
+ runCopilotAction(setCopilotActionMessage);
163
237
  return;
164
238
  }
165
- if (selectedProvider.provider.id === "copilot" && input === "l") {
166
- setSelectedCopilotActionIndex((current) => (current + 1) % COPILOT_ACTIONS.length);
239
+ if (key.tab && !key.shift) {
240
+ changeSelectedProvider(1);
167
241
  return;
168
242
  }
169
- if (selectedProvider.provider.id === "copilot" && input === "h") {
170
- setSelectedCopilotActionIndex((current) => (current - 1 + COPILOT_ACTIONS.length) % COPILOT_ACTIONS.length);
243
+ if (key.tab && key.shift) {
244
+ changeSelectedProvider(-1);
171
245
  return;
172
246
  }
173
- if (key.rightArrow) {
174
- setSelectedDetailTabIndex((current) => (current + 1) % DETAIL_TABS.length);
247
+ if (input === "]") {
248
+ changeSelectedDetailView(1);
175
249
  return;
176
250
  }
177
- if (key.leftArrow) {
178
- setSelectedDetailTabIndex((current) => (current - 1 + DETAIL_TABS.length) % DETAIL_TABS.length);
251
+ if (input === "[") {
252
+ changeSelectedDetailView(-1);
179
253
  return;
180
254
  }
181
- if ((key.tab && !key.shift) || input === "]") {
182
- setSelectedProviderId(sortedProviderStates[(selectedProviderIndex + 1) % sortedProviderStates.length].provider.id);
183
- setHasUserSelectedProvider(true);
255
+ if (key.rightArrow) {
256
+ changeSelectedSectionValue(1);
184
257
  return;
185
258
  }
186
- if ((key.tab && key.shift) || input === "[") {
187
- setSelectedProviderId(sortedProviderStates[(selectedProviderIndex - 1 + sortedProviderStates.length) % sortedProviderStates.length]
188
- .provider.id);
189
- setHasUserSelectedProvider(true);
259
+ if (key.leftArrow) {
260
+ changeSelectedSectionValue(-1);
190
261
  return;
191
262
  }
192
- if (key.downArrow || input === "j") {
193
- moveSelectedTableRow(1);
263
+ if (key.downArrow) {
264
+ moveSelectionDown();
194
265
  return;
195
266
  }
196
- if (key.upArrow || input === "k") {
197
- moveSelectedTableRow(-1);
267
+ if (key.upArrow) {
268
+ moveSelectionUp();
198
269
  }
199
270
  });
200
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "Provider " }), sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status, regionRef: getRegionRef(`provider:${state.provider.id}`) }, state.provider.id)))] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "View " }), DETAIL_TABS.map((tab, index) => (_jsx(DetailTab, { label: tab.label, active: index === selectedDetailTabIndex, regionRef: getRegionRef(`vtab:${index}`) }, tab.id)))] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) }) }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedDetailTab.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, { 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] }), _jsx(Text, { color: "gray", children: "Tab provider \u00B7 \u2190/\u2192 view \u00B7 \u2191/\u2193 row \u00B7 q quit" })] }));
271
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, { children: [_jsx(ControlSectionLabel, { label: "Provider", active: selectedControlSection === "provider" }), sortedProviderStates.map((state) => (_jsx(ProviderOption, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status, regionRef: getRegionRef(`provider:${state.provider.id}`) }, state.provider.id)))] }), _jsxs(Box, { children: [_jsx(ControlSectionLabel, { label: "View", active: selectedControlSection === "view" }), DETAIL_VIEWS.map((view, index) => (_jsx(ViewOption, { label: view.label, active: index === selectedDetailViewIndex, regionRef: getRegionRef(`view:${index}`) }, view.id)))] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, viewId: selectedDetailView.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) }) }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, viewId: selectedDetailView.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, { 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] }), _jsx(Text, { color: "gray", children: "\u2191/\u2193 move \u00B7 \u2190/\u2192 selected \u00B7 Tab provider \u00B7 [/] view \u00B7 q quit" })] }));
201
272
  }
202
273
  function CopilotActionsPanel(props) {
203
274
  if (props.providerState.provider.id !== "copilot") {
@@ -205,9 +276,9 @@ function CopilotActionsPanel(props) {
205
276
  }
206
277
  const hasNoUsage = props.providerState.status === "ready" && props.providerState.stats.summary.tokenEvents === 0;
207
278
  const accentColor = hasNoUsage ? "red" : "cyan";
208
- return (_jsxs(Box, { borderStyle: "round", borderColor: accentColor, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "Copilot setup" }), _jsx(Box, { children: COPILOT_ACTIONS.map((action, index) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: index === (props.selectedActionIndex ?? 0), bold: hasNoUsage && action.id === "vscode", color: action.enabled ? (hasNoUsage && action.id === "vscode" ? accentColor : undefined) : "gray", children: `${index + 1} ${action.label}` }) }, action.id))) }), _jsx(Text, { color: hasNoUsage ? accentColor : "gray", children: "Press 1 or h/l to select an action, enter to run selected." }), props.actionMessage ? _jsx(Text, { children: props.actionMessage }) : null] }));
279
+ return (_jsxs(Box, { borderStyle: "round", borderColor: accentColor, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "Copilot setup" }), _jsx(Box, { children: COPILOT_ACTIONS.map((action, index) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: index === (props.selectedActionIndex ?? 0), bold: hasNoUsage && action.id === "vscode", color: action.enabled ? (hasNoUsage && action.id === "vscode" ? accentColor : undefined) : "gray", children: `${index + 1} ${action.label}` }) }, action.id))) }), _jsx(Text, { color: hasNoUsage ? accentColor : "gray", children: "Press 1 to select an action, enter to run selected." }), props.actionMessage ? _jsx(Text, { children: props.actionMessage }) : null] }));
209
280
  }
210
- function runCopilotAction(actionId, setCopilotActionMessage) {
281
+ function runCopilotAction(setCopilotActionMessage) {
211
282
  setCopilotActionMessage("Updating VS Code settings...");
212
283
  void configureCopilotVsCodeLogging()
213
284
  .then((result) => {
@@ -231,14 +302,17 @@ function formatCopilotLoggingResult(result) {
231
302
  `"github.copilot.chat.otel.outfile": "${result.outfile}"`
232
303
  ].join("\n");
233
304
  }
234
- function ProviderTab(props) {
305
+ function ProviderOption(props) {
235
306
  const statusColor = props.status === "error" ? "red" : props.status === "loading" ? "yellow" : "green";
236
- const tabLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
237
- return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { color: statusColor, bold: props.active, children: tabLabel }) }));
307
+ const optionLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
308
+ return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { color: statusColor, bold: props.active, children: optionLabel }) }));
309
+ }
310
+ function ControlSectionLabel(props) {
311
+ return (_jsxs(Text, { color: props.active ? "cyan" : "gray", bold: props.active, children: [props.active ? "› " : " ", pad(props.label, 10)] }));
238
312
  }
239
- function DetailTab(props) {
240
- const tabLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
241
- return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { wrap: "truncate-end", bold: props.active, children: tabLabel }) }));
313
+ function ViewOption(props) {
314
+ const optionLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
315
+ return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { wrap: "truncate-end", bold: props.active, children: optionLabel }) }));
242
316
  }
243
317
  function SummaryPanel(props) {
244
318
  const { summary } = props.stats;
@@ -261,13 +335,13 @@ function ContentPanel(props) {
261
335
  if (props.providerState.status === "error") {
262
336
  return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
263
337
  }
264
- if (props.tabId === "limit-windows") {
338
+ if (props.viewId === "limit-windows") {
265
339
  return (_jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey, availableHeight: props.availableHeight }));
266
340
  }
267
- if (props.tabId === "summary") {
341
+ if (props.viewId === "summary") {
268
342
  return _jsx(SummaryPanel, { stats: props.providerState.stats });
269
343
  }
270
- if (props.tabId === "day-to-day-analyses") {
344
+ if (props.viewId === "day-to-day-analyses") {
271
345
  return (_jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey, availableHeight: props.availableHeight }));
272
346
  }
273
347
  return (_jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId, availableHeight: props.availableHeight }));
@@ -417,7 +491,10 @@ function buildLimitWindowTableRow(window) {
417
491
  formatCompactLocalDateTime(window.endTimeUtcIso),
418
492
  // Status-aware: shows "-" when the API-equivalent cost is unknown rather
419
493
  // than a misleading $0.00.
420
- formatUsageUsd(window.totals)
494
+ formatUsageUsd(window.totals),
495
+ // Extrapolated full value of the limit, rounded to a single figure here;
496
+ // the details panel shows the unrounded ±1% range.
497
+ formatLimitFullValueCompact(window.totals, window.maxUsedPercent)
421
498
  ]
422
499
  };
423
500
  }
@@ -459,15 +536,15 @@ function SelectionDetailsPanel(props) {
459
536
  if (props.providerState.status !== "ready") {
460
537
  return null;
461
538
  }
462
- if (props.tabId === "limit-windows" && props.selectedLimitRow) {
539
+ if (props.viewId === "limit-windows" && props.selectedLimitRow) {
463
540
  const row = props.selectedLimitRow;
464
- return (_jsx(DetailsPanelFrame, { children: _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }) }));
541
+ return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals), note: "API equivalent cost" }), _jsx(DetailRow, { label: "Full value", value: formatLimitFullValueRange(row.totals, row.maxUsedPercent), note: "API equivalent cost if 100% will be used" })] }));
465
542
  }
466
- if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
543
+ if (props.viewId === "day-to-day-analyses" && props.selectedDayRow) {
467
544
  const row = props.selectedDayRow;
468
545
  return (_jsxs(DetailsPanelFrame, { children: [_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 })] }));
469
546
  }
470
- if (props.tabId === "usage-by-model" && props.selectedModelRow) {
547
+ if (props.viewId === "usage-by-model" && props.selectedModelRow) {
471
548
  return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Text, { children: ["model: ", props.selectedModelRow.modelId, " events: ", formatInteger(props.selectedModelRow.totals.eventCount)] }), _jsx(UsageTotalsDetails, { totals: props.selectedModelRow.totals, modelId: props.selectedModelRow.modelId })] }));
472
549
  }
473
550
  return null;
@@ -479,7 +556,7 @@ function DetailRow(props) {
479
556
  const labelText = props.noSlice
480
557
  ? props.label.padEnd(props.padLength ?? 14)
481
558
  : pad(props.label, props.padLength ?? 14);
482
- return (_jsxs(Text, { children: [labelText, props.value] }));
559
+ return (_jsxs(Text, { children: [labelText, props.value, props.note ? _jsx(Text, { color: "gray", children: ` ${props.note}` }) : null] }));
483
560
  }
484
561
  function UsageTotalsDetails(props) {
485
562
  const { totals } = props;
@@ -530,21 +607,6 @@ function formatCompactNumber(value) {
530
607
  minimumFractionDigits: 0
531
608
  });
532
609
  }
533
- function formatCredits(value) {
534
- if (value > 0 && value < 0.01) {
535
- return "<0.01";
536
- }
537
- return value.toLocaleString("en-US", {
538
- minimumFractionDigits: 2,
539
- maximumFractionDigits: 2
540
- });
541
- }
542
- function formatUsageCredits(totals, modelId) {
543
- if (isInternalUsageModel(modelId)) {
544
- return "N/A";
545
- }
546
- return totals.estimatedCreditsStatus === "unavailable" ? "-" : formatCredits(totals.estimatedCredits);
547
- }
548
610
  function formatUsageUsd(totals, modelId) {
549
611
  if (isInternalUsageModel(modelId)) {
550
612
  return "N/A";
@@ -565,6 +627,37 @@ function formatUsd(value) {
565
627
  maximumFractionDigits: 2
566
628
  });
567
629
  }
630
+ function formatUsdWhole(value) {
631
+ return Math.round(value).toLocaleString("en-US", {
632
+ currency: "USD",
633
+ style: "currency",
634
+ minimumFractionDigits: 0,
635
+ maximumFractionDigits: 0
636
+ });
637
+ }
638
+ // Extrapolate the limit's full USD value from the observed API-equivalent cost
639
+ // and how much of the limit it represents. Uses the highest reported percent
640
+ // (the latest cumulative usage) and returns "-" when the cost is unknown or the
641
+ // percent is missing.
642
+ function limitFullValueUsd(totals, usedPercent) {
643
+ if (totals.estimatedCreditsStatus === "unavailable") {
644
+ return null;
645
+ }
646
+ const usedUsd = totals.estimatedCredits * CODEX_CREDIT_COST_USD;
647
+ return estimateLimitFullValue(usedUsd, usedPercent);
648
+ }
649
+ function formatLimitFullValueCompact(totals, usedPercent) {
650
+ const estimate = limitFullValueUsd(totals, usedPercent);
651
+ return estimate ? formatUsdWhole(estimate.point) : "-";
652
+ }
653
+ function formatLimitFullValueRange(totals, usedPercent) {
654
+ const estimate = limitFullValueUsd(totals, usedPercent);
655
+ if (!estimate) {
656
+ return "-";
657
+ }
658
+ const low = formatUsd(estimate.low);
659
+ return Number.isFinite(estimate.high) ? `${low} – ${formatUsd(estimate.high)}` : `≥ ${low}`;
660
+ }
568
661
  function formatUnitUsd(value) {
569
662
  if (!Number.isFinite(value)) {
570
663
  return "-";
@@ -629,13 +722,6 @@ function formatUsedPercentRange(minUsedPercent, maxUsedPercent) {
629
722
  ? fmt(minUsedPercent)
630
723
  : `${fmt(minUsedPercent)}–${fmt(maxUsedPercent)}`;
631
724
  }
632
- function formatWindowMinutes(value) {
633
- const hours = value / 60;
634
- if (hours >= 24) {
635
- return `${(hours / 24).toFixed(2)}d`;
636
- }
637
- return `${hours.toFixed(2)}h`;
638
- }
639
725
  function formatCompactWindowMinutes(value) {
640
726
  const hours = value / 60;
641
727
  if (hours >= 24) {
@@ -808,7 +894,7 @@ function parseMouseClicks(chunk) {
808
894
  }
809
895
  return clicks;
810
896
  }
811
- // Tracks the on-screen rectangle of named clickable regions (the tabs) via Ink
897
+ // Tracks the on-screen rectangle of named clickable regions via Ink
812
898
  // refs and resolves a click coordinate back to a region id.
813
899
  function useClickRegions() {
814
900
  const nodesRef = useRef(new Map());
@@ -931,6 +1017,12 @@ function clampSelectionIndex(value, rowCount) {
931
1017
  }
932
1018
  return Math.max(0, Math.min(value, rowCount - 1));
933
1019
  }
1020
+ function clampOptionalSelectionIndex(value, rowCount) {
1021
+ if (value === undefined) {
1022
+ return -1;
1023
+ }
1024
+ return clampSelectionIndex(value, rowCount);
1025
+ }
934
1026
  function sortProviderStatesByUsage(states) {
935
1027
  return states
936
1028
  .map((state, index) => ({ state, index }))
@@ -236,10 +236,20 @@ function clampPercent(value) {
236
236
  function deduplicateRecords(records) {
237
237
  const byKey = new Map();
238
238
  for (const record of records) {
239
- byKey.set(`${record.sessionId}:${record.responseId}`, record);
239
+ const key = `${record.sessionId}:${record.responseId}`;
240
+ const existing = byKey.get(key);
241
+ // The RPC may surface the same response more than once (e.g. progressive
242
+ // snapshots in unspecified order). Keep the largest coherent total rather
243
+ // than trusting iteration order, so we never undercount a final snapshot.
244
+ if (!existing || recordTokenTotal(record) > recordTokenTotal(existing)) {
245
+ byKey.set(key, record);
246
+ }
240
247
  }
241
248
  return [...byKey.values()];
242
249
  }
250
+ function recordTokenTotal(record) {
251
+ return record.input + record.cacheRead + record.cacheWrite + record.output;
252
+ }
243
253
  function usageRecordToTotals(modelId, record) {
244
254
  return {
245
255
  inputTokens: record.input,
@@ -255,11 +265,12 @@ function usageRecordToTotals(modelId, record) {
255
265
  record.output,
256
266
  estimatedCredits: creditsFor(modelId, record),
257
267
  eventCount: 1,
258
- // The local RPC reports cache reads but never cache writes, so cache reads
259
- // are accurate while cache writes are genuinely unknown (not a confirmed
260
- // zero) surfaced as "-" everywhere, including the input/output ratio.
268
+ // The local RPC reports cache reads but never cache writes, so a zero cache
269
+ // write is genuinely unknown (not a confirmed zero) and is surfaced as "-".
270
+ // A positive value only appears when a source explicitly reports it, in
271
+ // which case it is both billed (see creditsFor) and shown as known.
261
272
  cacheReadStatus: "known",
262
- cacheWriteStatus: "unavailable",
273
+ cacheWriteStatus: record.cacheWrite > 0 ? "known" : "unavailable",
263
274
  estimatedCreditsStatus: rateForModel(modelId, record.input)
264
275
  ? "known"
265
276
  : "unavailable"
@@ -240,7 +240,9 @@ function creditsFor(modelId, usage, timestampMs) {
240
240
  return 0;
241
241
  }
242
242
  const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
243
- const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
243
+ // The US inference surcharge must match regardless of the casing the source
244
+ // reports (e.g. "us", "US"), so compare case-insensitively.
245
+ const inferenceMultiplier = usage.inferenceGeo.trim().toLowerCase() === "us" ? 1.1 : 1;
244
246
  return (((usage.inputTokens / 1000000) * rate.input +
245
247
  (usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
246
248
  (cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
@@ -566,15 +568,20 @@ function mergeParsedUsageEvents(previous, next) {
566
568
  rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
567
569
  };
568
570
  }
569
- // Pick the snapshot that carries the most usage. Cumulative snapshots are monotonic, so the
570
- // largest total is the final state; this also keeps a real synthetic-followup row (0 tokens)
571
- // from clobbering the real usage it follows. Ties fall back to the later, then the earlier-seen
572
- // event for deterministic output.
571
+ // Pick the snapshot with the latest timestamp. Same-key events are repeated/streamed snapshots
572
+ // of one logical request, so the most recent one reflects the final state. The one exception is
573
+ // a zero-usage internal <synthetic> completion marker, which is a real followup row rather than
574
+ // an updated snapshot and must not clobber the real usage it follows.
573
575
  function selectMergedSnapshotEvent(previous, next) {
574
- if (next.totals.totalTokens !== previous.totals.totalTokens) {
575
- return next.totals.totalTokens > previous.totals.totalTokens ? next : previous;
576
+ const previousIsHollowSynthetic = isInternalClaudeModel(previous.modelId) && previous.totals.totalTokens === 0;
577
+ const nextIsHollowSynthetic = isInternalClaudeModel(next.modelId) && next.totals.totalTokens === 0;
578
+ if (nextIsHollowSynthetic && !previousIsHollowSynthetic) {
579
+ return previous;
576
580
  }
577
- return normalizeTimestamp(next.timestampMs) > normalizeTimestamp(previous.timestampMs) ? next : previous;
581
+ if (previousIsHollowSynthetic && !nextIsHollowSynthetic) {
582
+ return next;
583
+ }
584
+ return normalizeTimestamp(next.timestampMs) >= normalizeTimestamp(previous.timestampMs) ? next : previous;
578
585
  }
579
586
  function selectMergedEventModelId(primary, other) {
580
587
  if (primary.modelId === other.modelId) {
@@ -7,10 +7,16 @@ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTota
7
7
  import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
8
8
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
9
9
  import { resolveUsageRate } from "./pricing.js";
10
+ // One credit equals $0.01 (see CODEX_CREDIT_COST_USD in index.tsx), so credits
11
+ // equal USD * 100. Rate cards are expressed in the model's actual API price in
12
+ // USD per 1M tokens and scaled to credits in creditsFor, matching the Claude
13
+ // provider. These are the real gpt-5.* API prices, not the (4x cheaper) Codex
14
+ // subscription credit prices.
15
+ const USD_TO_CREDITS = 100;
10
16
  const RATE_CARD = {
11
- "gpt-5.5": { input: 125, cacheRead: 12.5, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 125, output: 750 },
12
- "gpt-5.4": { input: 62.5, cacheRead: 6.25, cacheWrite: 62.5, cacheWrite5m: 62.5, cacheWrite1h: 62.5, output: 375 },
13
- "gpt-5.4-mini": { input: 18.75, cacheRead: 1.875, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 18.75, output: 113 }
17
+ "gpt-5.5": { input: 5, cacheRead: 0.5, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 30 },
18
+ "gpt-5.4": { input: 2.5, cacheRead: 0.25, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 15 },
19
+ "gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0.75, cacheWrite5m: 0.75, cacheWrite1h: 0.75, output: 4.5 }
14
20
  };
15
21
  export class CodexUsageProvider extends UsageProviderBase {
16
22
  constructor(options = {}) {
@@ -235,9 +241,10 @@ function creditsFor(modelId, usage) {
235
241
  }
236
242
  const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
237
243
  const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
238
- return ((nonCachedInputTokens / 1000000) * rate.input +
244
+ return (((nonCachedInputTokens / 1000000) * rate.input +
239
245
  (cachedInputTokens / 1000000) * rate.cacheRead +
240
- (usage.outputTokens / 1000000) * rate.output);
246
+ (usage.outputTokens / 1000000) * rate.output) *
247
+ USD_TO_CREDITS);
241
248
  }
242
249
  function rawUsageToTotals(usage) {
243
250
  const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
@@ -3,11 +3,42 @@ export function createLimitWindowAggregates() {
3
3
  return new Map();
4
4
  }
5
5
  export function numberOrZero(value) {
6
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
6
+ if (typeof value === "number") {
7
+ return Number.isFinite(value) ? value : 0;
8
+ }
9
+ if (typeof value === "string") {
10
+ const trimmed = value.trim();
11
+ if (trimmed === "") {
12
+ return 0;
13
+ }
14
+ const parsed = Number(trimmed);
15
+ return Number.isFinite(parsed) ? parsed : 0;
16
+ }
17
+ return 0;
7
18
  }
8
19
  export function asRecord(value) {
9
20
  return value && typeof value === "object" ? value : null;
10
21
  }
22
+ /**
23
+ * Extrapolate the full value of a limit from a partial observation: if
24
+ * `usedValue` represents `usedPercent` of the limit, the full limit is
25
+ * `usedValue / (usedPercent / 100)`. Because `usedPercent` is only known
26
+ * approximately, a `±percentTolerance` band yields a low/high range around the
27
+ * point estimate. Returns null when there is nothing to extrapolate from
28
+ * (no observed value, or a non-positive percent).
29
+ */
30
+ export function estimateLimitFullValue(usedValue, usedPercent, percentTolerance = 1) {
31
+ if (!(usedValue > 0) || !(usedPercent > 0)) {
32
+ return null;
33
+ }
34
+ const toFull = (percent) => usedValue / (percent / 100);
35
+ const upperPercent = usedPercent - percentTolerance;
36
+ return {
37
+ point: toFull(usedPercent),
38
+ low: toFull(usedPercent + percentTolerance),
39
+ high: upperPercent > 0 ? toFull(upperPercent) : Infinity
40
+ };
41
+ }
11
42
  export function applyRateLimits(windows, rateLimits, eventTimeMs, modelId, deltaTotals, planTypes) {
12
43
  if (!rateLimits) {
13
44
  return;
@@ -4,7 +4,9 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  const REPORTING_ENDPOINT = "https://devforth.io/admin/api/report_ussage_anonymous";
6
6
  const CREDIT_TO_DOLLARS = 0.01;
7
- const MIN_REPORTED_USED_PERCENTS = 1;
7
+ // Limit windows at or below this used-percent carry too little signal to be
8
+ // worth reporting, so they are dropped from the anonymous usage payload.
9
+ const SKIP_REPORT_USED_PERCENTS = 3;
8
10
  let versionCache = null;
9
11
  export async function reportAnonymousUsage(statsList) {
10
12
  const payload = await buildAnonymousUsagePayload(statsList);
@@ -103,7 +105,7 @@ function resolveReportedUsedPercents(window) {
103
105
  return clampPercent(window.maxUsedPercent - window.minUsedPercent);
104
106
  }
105
107
  function shouldReportUsageWindow(window) {
106
- return resolveReportedUsedPercents(window) >= MIN_REPORTED_USED_PERCENTS;
108
+ return resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS;
107
109
  }
108
110
  function clampPercent(value) {
109
111
  if (!Number.isFinite(value)) {
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
5
5
  "author": "Devforth (https://devforth.io)",
6
6
  "license": "MIT",
7
- "packageManager": "pnpm@10.28.2",
8
7
  "type": "commonjs",
9
8
  "main": "./dist/index.js",
10
9
  "bin": {
@@ -30,16 +29,6 @@
30
29
  "publishConfig": {
31
30
  "access": "public"
32
31
  },
33
- "scripts": {
34
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
35
- "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
36
- "prepack": "npm run build",
37
- "prestart": "npm run build",
38
- "start": "node ./bin/letmecode.js",
39
- "pretest": "npm run build",
40
- "smoke": "node ./bin/letmecode.js",
41
- "test": "node --test ink-app/test/*.test.mjs"
42
- },
43
32
  "keywords": [
44
33
  "ai",
45
34
  "agents",
@@ -64,5 +53,14 @@
64
53
  "@types/node": "^24.0.7",
65
54
  "@types/react": "^18.3.24",
66
55
  "typescript": "^5.8.3"
56
+ },
57
+ "scripts": {
58
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
59
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
60
+ "prestart": "npm run build",
61
+ "start": "node ./bin/letmecode.js",
62
+ "pretest": "npm run build",
63
+ "smoke": "node ./bin/letmecode.js",
64
+ "test": "node --test ink-app/test/*.test.mjs"
67
65
  }
68
- }
66
+ }