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.
- package/ink-app/dist/cli-options.js +6 -6
- package/ink-app/dist/index.js +176 -84
- package/ink-app/dist/providers/antigravity/provider.js +16 -5
- package/ink-app/dist/providers/claude.js +15 -8
- package/ink-app/dist/providers/codex.js +12 -5
- package/ink-app/dist/providers/limits.js +32 -1
- package/ink-app/dist/reporting.js +4 -2
- package/package.json +11 -13
|
@@ -58,12 +58,12 @@ export function buildHelpText() {
|
|
|
58
58
|
" --no-usage Disable anonymous usage reporting",
|
|
59
59
|
"",
|
|
60
60
|
"Controls:",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
" 1,
|
|
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:",
|
package/ink-app/dist/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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 [
|
|
42
|
-
const [
|
|
43
|
-
const [
|
|
44
|
-
const [
|
|
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
|
|
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 =
|
|
56
|
-
const activeDayRowIndex =
|
|
57
|
-
const activeModelRowIndex =
|
|
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("
|
|
123
|
-
|
|
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 (
|
|
128
|
-
|
|
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 (
|
|
132
|
-
|
|
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 (
|
|
136
|
-
|
|
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
|
-
|
|
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(
|
|
236
|
+
runCopilotAction(setCopilotActionMessage);
|
|
163
237
|
return;
|
|
164
238
|
}
|
|
165
|
-
if (
|
|
166
|
-
|
|
239
|
+
if (key.tab && !key.shift) {
|
|
240
|
+
changeSelectedProvider(1);
|
|
167
241
|
return;
|
|
168
242
|
}
|
|
169
|
-
if (
|
|
170
|
-
|
|
243
|
+
if (key.tab && key.shift) {
|
|
244
|
+
changeSelectedProvider(-1);
|
|
171
245
|
return;
|
|
172
246
|
}
|
|
173
|
-
if (
|
|
174
|
-
|
|
247
|
+
if (input === "]") {
|
|
248
|
+
changeSelectedDetailView(1);
|
|
175
249
|
return;
|
|
176
250
|
}
|
|
177
|
-
if (
|
|
178
|
-
|
|
251
|
+
if (input === "[") {
|
|
252
|
+
changeSelectedDetailView(-1);
|
|
179
253
|
return;
|
|
180
254
|
}
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
setHasUserSelectedProvider(true);
|
|
255
|
+
if (key.rightArrow) {
|
|
256
|
+
changeSelectedSectionValue(1);
|
|
184
257
|
return;
|
|
185
258
|
}
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
.provider.id);
|
|
189
|
-
setHasUserSelectedProvider(true);
|
|
259
|
+
if (key.leftArrow) {
|
|
260
|
+
changeSelectedSectionValue(-1);
|
|
190
261
|
return;
|
|
191
262
|
}
|
|
192
|
-
if (key.downArrow
|
|
193
|
-
|
|
263
|
+
if (key.downArrow) {
|
|
264
|
+
moveSelectionDown();
|
|
194
265
|
return;
|
|
195
266
|
}
|
|
196
|
-
if (key.upArrow
|
|
197
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
305
|
+
function ProviderOption(props) {
|
|
235
306
|
const statusColor = props.status === "error" ? "red" : props.status === "loading" ? "yellow" : "green";
|
|
236
|
-
const
|
|
237
|
-
return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { color: statusColor, bold: props.active, children:
|
|
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
|
|
240
|
-
const
|
|
241
|
-
return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { wrap: "truncate-end", bold: props.active, children:
|
|
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.
|
|
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.
|
|
341
|
+
if (props.viewId === "summary") {
|
|
268
342
|
return _jsx(SummaryPanel, { stats: props.providerState.stats });
|
|
269
343
|
}
|
|
270
|
-
if (props.
|
|
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.
|
|
539
|
+
if (props.viewId === "limit-windows" && props.selectedLimitRow) {
|
|
463
540
|
const row = props.selectedLimitRow;
|
|
464
|
-
return (
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
259
|
-
//
|
|
260
|
-
//
|
|
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
|
-
|
|
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
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
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
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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:
|
|
12
|
-
"gpt-5.4": { input:
|
|
13
|
-
"gpt-5.4-mini": { input:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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
|
+
}
|