letmecode 0.1.13 → 0.1.16
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/index.js +140 -160
- package/ink-app/dist/providers/antigravity.js +12 -2
- package/ink-app/dist/providers/claude.js +84 -22
- package/ink-app/dist/reporting.js +11 -2
- package/package.json +13 -11
package/ink-app/dist/index.js
CHANGED
|
@@ -19,31 +19,9 @@ const DETAIL_TABS = [
|
|
|
19
19
|
{ id: "usage-by-model", label: "Models" }
|
|
20
20
|
];
|
|
21
21
|
const CODEX_CREDIT_COST_USD = 0.01;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{ header: "Used", width: 8 },
|
|
26
|
-
{ header: "Start", width: 14 },
|
|
27
|
-
{ header: "End", width: 14 },
|
|
28
|
-
{ header: "API eq.", width: 8 }
|
|
29
|
-
];
|
|
30
|
-
const DAILY_TABLE_COLUMNS = [
|
|
31
|
-
{ header: "Day", width: 9 },
|
|
32
|
-
{ header: "Ev", width: 5 },
|
|
33
|
-
{ header: "Input", width: 8 },
|
|
34
|
-
{ header: "Output", width: 7 },
|
|
35
|
-
{ header: "C read", width: 8 },
|
|
36
|
-
{ header: "C write", width: 7 },
|
|
37
|
-
{ header: "API eq.", width: 7 }
|
|
38
|
-
];
|
|
39
|
-
const MODEL_TABLE_COLUMNS = [
|
|
40
|
-
{ header: "Model", width: 16 },
|
|
41
|
-
{ header: "Input", width: 7 },
|
|
42
|
-
{ header: "Output", width: 7 },
|
|
43
|
-
{ header: "C read", width: 7 },
|
|
44
|
-
{ header: "C write", width: 7 },
|
|
45
|
-
{ header: "API eq.", width: 7 }
|
|
46
|
-
];
|
|
22
|
+
const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Window", "Used", "Start", "End", "API eq."];
|
|
23
|
+
const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
|
|
24
|
+
const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
|
|
47
25
|
const COPILOT_ACTIONS = [
|
|
48
26
|
{ id: "vscode", label: "Start logging VS Code", enabled: true }
|
|
49
27
|
];
|
|
@@ -217,7 +195,7 @@ function App(props) {
|
|
|
217
195
|
moveSelectedTableRow(-1);
|
|
218
196
|
}
|
|
219
197
|
});
|
|
220
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "cyan", children: "
|
|
198
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsxs(Box, { marginTop: 1, 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, { marginTop: 1, 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, { 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] }), _jsx(Text, { color: "gray", children: "Tab provider \u00B7 \u2190/\u2192 view \u00B7 \u2191/\u2193 row \u00B7 q quit" })] }));
|
|
221
199
|
}
|
|
222
200
|
function CopilotActionsPanel(props) {
|
|
223
201
|
if (props.providerState.provider.id !== "copilot") {
|
|
@@ -290,27 +268,23 @@ function ContentPanel(props) {
|
|
|
290
268
|
return (_jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId, availableHeight: props.availableHeight }));
|
|
291
269
|
}
|
|
292
270
|
function LimitWindowsPanel(props) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
{ key: "section-gap", text: "" },
|
|
296
|
-
...buildLimitWindowTableLines("secondary", "Secondary limits", props.stats.secondaryLimitWindows, props.selectedRowKey)
|
|
297
|
-
];
|
|
298
|
-
return (_jsx(ScrollableLineViewport, { bodyLines: bodyLines, availableHeight: props.availableHeight }));
|
|
271
|
+
const tableLines = buildLimitWindowTableLines(props.stats, props.selectedRowKey);
|
|
272
|
+
return (_jsx(ScrollableLineViewport, { ...tableLines, selectedBodyLineKey: props.selectedRowKey ? `limit-row:${props.selectedRowKey}` : undefined, availableHeight: props.availableHeight }));
|
|
299
273
|
}
|
|
300
274
|
function UsageByModelPanel(props) {
|
|
301
275
|
if (props.stats.modelUsage.length === 0) {
|
|
302
276
|
return _jsx(Text, { color: "gray", children: "No model usage found." });
|
|
303
277
|
}
|
|
304
278
|
const totals = props.stats.summary.totals;
|
|
305
|
-
const
|
|
306
|
-
return (_jsx(ScrollableLineViewport, {
|
|
279
|
+
const tableLines = buildModelUsageTableLines(props.stats.modelUsage, totals, props.selectedModelId);
|
|
280
|
+
return (_jsx(ScrollableLineViewport, { ...tableLines, selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
|
|
307
281
|
}
|
|
308
282
|
function DayToDayPanel(props) {
|
|
309
283
|
if (props.stats.dayUsage.length === 0) {
|
|
310
284
|
return _jsx(Text, { color: "gray", children: "No day-by-day usage found." });
|
|
311
285
|
}
|
|
312
|
-
const
|
|
313
|
-
return (_jsx(ScrollableLineViewport, {
|
|
286
|
+
const tableLines = buildDailyUsageTableLines(props.stats.dayUsage, props.selectedDayKey);
|
|
287
|
+
return (_jsx(ScrollableLineViewport, { ...tableLines, selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
|
|
314
288
|
}
|
|
315
289
|
function ScrollableLineViewport(props) {
|
|
316
290
|
const headerLines = props.headerLines ?? [];
|
|
@@ -330,147 +304,153 @@ function ScrollableLineViewport(props) {
|
|
|
330
304
|
function ScrollableViewportLine(props) {
|
|
331
305
|
return (_jsx(Text, { bold: props.line.bold, color: props.line.color, inverse: props.line.inverse, wrap: "truncate-end", children: props.line.text }));
|
|
332
306
|
}
|
|
333
|
-
function
|
|
334
|
-
return
|
|
307
|
+
function createTextTable(headers, rows) {
|
|
308
|
+
return {
|
|
309
|
+
headers: [...headers],
|
|
310
|
+
widths: headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)))
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function buildTableBorder(table, left, middle, right) {
|
|
314
|
+
return `${left}${table.widths.map((width) => "─".repeat(width + 2)).join(middle)}${right}`;
|
|
335
315
|
}
|
|
336
|
-
function buildTableRow(
|
|
337
|
-
return `│${
|
|
316
|
+
function buildTableRow(table, cells) {
|
|
317
|
+
return `│${table.widths.map((width, index) => ` ${pad(cells[index] ?? "", width)} `).join("│")}│`;
|
|
338
318
|
}
|
|
339
|
-
function
|
|
340
|
-
if (
|
|
341
|
-
return
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
319
|
+
function buildTextTableLines(options) {
|
|
320
|
+
if (options.rows.length === 0 && !options.totalRow) {
|
|
321
|
+
return {
|
|
322
|
+
bodyLines: [
|
|
323
|
+
{ key: `${options.lineKeyPrefix}-title`, text: options.title, bold: true },
|
|
324
|
+
{ key: `${options.lineKeyPrefix}-empty`, text: "No rows found.", color: "gray" }
|
|
325
|
+
]
|
|
326
|
+
};
|
|
345
327
|
}
|
|
346
|
-
|
|
347
|
-
|
|
328
|
+
const tableRows = options.totalRow
|
|
329
|
+
? [...options.rows, options.totalRow]
|
|
330
|
+
: options.rows;
|
|
331
|
+
const table = createTextTable(options.headers, tableRows.map((row) => row.cells));
|
|
332
|
+
const headerLines = [
|
|
333
|
+
{ key: `${options.lineKeyPrefix}-title`, text: options.title, bold: true },
|
|
348
334
|
{
|
|
349
|
-
key: `${
|
|
350
|
-
text: buildTableBorder(
|
|
335
|
+
key: `${options.lineKeyPrefix}-top-border`,
|
|
336
|
+
text: buildTableBorder(table, "┌", "┬", "┐"),
|
|
351
337
|
color: "gray"
|
|
352
338
|
},
|
|
353
339
|
{
|
|
354
|
-
key: `${
|
|
355
|
-
text: buildTableRow(
|
|
340
|
+
key: `${options.lineKeyPrefix}-header`,
|
|
341
|
+
text: buildTableRow(table, table.headers),
|
|
356
342
|
color: "gray"
|
|
357
343
|
},
|
|
358
344
|
{
|
|
359
|
-
key: `${
|
|
360
|
-
text: buildTableBorder(
|
|
361
|
-
color: "gray"
|
|
362
|
-
},
|
|
363
|
-
...windows.map((window) => {
|
|
364
|
-
const lineKey = getLimitRowKey(window);
|
|
365
|
-
const windowLabel = formatCompactWindowMinutes(window.windowMinutes);
|
|
366
|
-
const usedLabel = formatUsedPercentRange(window.minUsedPercent, window.maxUsedPercent);
|
|
367
|
-
const isSelected = selectedRowKey === lineKey;
|
|
368
|
-
return {
|
|
369
|
-
key: `limit-row:${lineKey}`,
|
|
370
|
-
text: buildTableRow(LIMIT_TABLE_COLUMNS, [
|
|
371
|
-
window.planType,
|
|
372
|
-
windowLabel,
|
|
373
|
-
usedLabel,
|
|
374
|
-
formatCompactLocalDateTime(window.startTimeUtcIso),
|
|
375
|
-
formatCompactLocalDateTime(window.endTimeUtcIso),
|
|
376
|
-
formatUsd(window.totals.estimatedCredits * CODEX_CREDIT_COST_USD)
|
|
377
|
-
]),
|
|
378
|
-
inverse: isSelected,
|
|
379
|
-
color: isSelected ? "cyan" : undefined
|
|
380
|
-
};
|
|
381
|
-
}),
|
|
382
|
-
{
|
|
383
|
-
key: `${scope}-bottom-border`,
|
|
384
|
-
text: buildTableBorder(LIMIT_TABLE_COLUMNS, "└", "┴", "┘"),
|
|
345
|
+
key: `${options.lineKeyPrefix}-header-border`,
|
|
346
|
+
text: buildTableBorder(table, "├", "┼", "┤"),
|
|
385
347
|
color: "gray"
|
|
386
348
|
}
|
|
387
349
|
];
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
key: "daily-top-border",
|
|
394
|
-
text: buildTableBorder(DAILY_TABLE_COLUMNS, "┌", "┬", "┐"),
|
|
395
|
-
color: "gray"
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
key: "daily-header",
|
|
399
|
-
text: buildTableRow(DAILY_TABLE_COLUMNS, DAILY_TABLE_COLUMNS.map((column) => column.header)),
|
|
400
|
-
color: "gray"
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
key: "daily-header-border",
|
|
404
|
-
text: buildTableBorder(DAILY_TABLE_COLUMNS, "├", "┼", "┤"),
|
|
405
|
-
color: "gray"
|
|
406
|
-
},
|
|
407
|
-
...rows.map((row) => {
|
|
408
|
-
const isSelected = selectedDayKey === row.dayKey;
|
|
409
|
-
return {
|
|
410
|
-
key: `day-row:${row.dayKey}`,
|
|
411
|
-
text: buildTableRow(DAILY_TABLE_COLUMNS, [
|
|
412
|
-
formatUtcDay(row.dayKey),
|
|
413
|
-
formatCompactTokenCount(row.totals.eventCount),
|
|
414
|
-
formatCompactTokenCount(row.totals.inputTokens),
|
|
415
|
-
formatCompactTokenCount(row.totals.outputTokens),
|
|
416
|
-
formatCompactCacheTokens(row.totals, row.totals.cacheReadInputTokens),
|
|
417
|
-
formatCompactCacheTokens(row.totals, row.totals.cacheWriteInputTokens),
|
|
418
|
-
formatUsageUsd(row.totals)
|
|
419
|
-
]),
|
|
350
|
+
const bodyLines = options.rows.flatMap((row) => {
|
|
351
|
+
const isSelected = options.selectedRowKey === row.key;
|
|
352
|
+
const lines = [{
|
|
353
|
+
key: row.key,
|
|
354
|
+
text: buildTableRow(table, row.cells),
|
|
420
355
|
inverse: isSelected,
|
|
421
356
|
color: isSelected ? "cyan" : undefined
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
357
|
+
}];
|
|
358
|
+
if (options.separatorAfterRowKeys?.has(row.key)) {
|
|
359
|
+
lines.push({
|
|
360
|
+
key: `${row.key}-separator`,
|
|
361
|
+
text: buildTableBorder(table, "├", "┼", "┤"),
|
|
362
|
+
color: "gray"
|
|
363
|
+
});
|
|
428
364
|
}
|
|
429
|
-
|
|
365
|
+
return lines;
|
|
366
|
+
});
|
|
367
|
+
const footerLines = [];
|
|
368
|
+
if (options.totalRow) {
|
|
369
|
+
footerLines.push({
|
|
370
|
+
key: `${options.lineKeyPrefix}-total-border`,
|
|
371
|
+
text: buildTableBorder(table, "├", "┼", "┤"),
|
|
372
|
+
color: "gray"
|
|
373
|
+
}, {
|
|
374
|
+
key: options.totalRow.key,
|
|
375
|
+
text: buildTableRow(table, options.totalRow.cells),
|
|
376
|
+
color: "cyan"
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
footerLines.push({
|
|
380
|
+
key: `${options.lineKeyPrefix}-bottom-border`,
|
|
381
|
+
text: buildTableBorder(table, "└", "┴", "┘"),
|
|
382
|
+
color: "gray"
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
headerLines,
|
|
386
|
+
bodyLines,
|
|
387
|
+
footerLines
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function buildLimitWindowTableLines(stats, selectedRowKey) {
|
|
391
|
+
const primaryRows = stats.primaryLimitWindows.map((window) => buildLimitWindowTableRow(window));
|
|
392
|
+
const secondaryRows = stats.secondaryLimitWindows.map((window) => buildLimitWindowTableRow(window));
|
|
393
|
+
const separatorAfterRowKeys = primaryRows.length > 0 && secondaryRows.length > 0
|
|
394
|
+
? new Set([primaryRows[primaryRows.length - 1].key])
|
|
395
|
+
: undefined;
|
|
396
|
+
return buildTextTableLines({
|
|
397
|
+
title: "Limits",
|
|
398
|
+
lineKeyPrefix: "limits",
|
|
399
|
+
headers: LIMIT_TABLE_HEADERS,
|
|
400
|
+
rows: [...primaryRows, ...secondaryRows],
|
|
401
|
+
selectedRowKey: selectedRowKey ? `limit-row:${selectedRowKey}` : undefined,
|
|
402
|
+
separatorAfterRowKeys
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function buildLimitWindowTableRow(window) {
|
|
406
|
+
return {
|
|
407
|
+
key: `limit-row:${getLimitRowKey(window)}`,
|
|
408
|
+
cells: [
|
|
409
|
+
window.scope,
|
|
410
|
+
window.planType,
|
|
411
|
+
formatCompactWindowMinutes(window.windowMinutes),
|
|
412
|
+
formatUsedPercentRange(window.minUsedPercent, window.maxUsedPercent),
|
|
413
|
+
formatCompactLocalDateTime(window.startTimeUtcIso),
|
|
414
|
+
formatCompactLocalDateTime(window.endTimeUtcIso),
|
|
415
|
+
formatUsd(window.totals.estimatedCredits * CODEX_CREDIT_COST_USD)
|
|
416
|
+
]
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function buildDailyUsageTableLines(rows, selectedDayKey) {
|
|
420
|
+
return buildTextTableLines({
|
|
421
|
+
title: "Daily usage",
|
|
422
|
+
lineKeyPrefix: "daily",
|
|
423
|
+
headers: DAILY_TABLE_HEADERS,
|
|
424
|
+
rows: rows.map((row) => ({
|
|
425
|
+
key: `day-row:${row.dayKey}`,
|
|
426
|
+
cells: [
|
|
427
|
+
formatUtcDay(row.dayKey),
|
|
428
|
+
formatCompactTokenCount(row.totals.eventCount),
|
|
429
|
+
formatCompactTokenCount(row.totals.inputTokens),
|
|
430
|
+
formatCompactTokenCount(row.totals.outputTokens),
|
|
431
|
+
formatCompactCacheTokens(row.totals, row.totals.cacheReadInputTokens),
|
|
432
|
+
formatCompactCacheTokens(row.totals, row.totals.cacheWriteInputTokens),
|
|
433
|
+
formatUsageUsd(row.totals)
|
|
434
|
+
]
|
|
435
|
+
})),
|
|
436
|
+
selectedRowKey: selectedDayKey ? `day-row:${selectedDayKey}` : undefined
|
|
437
|
+
});
|
|
430
438
|
}
|
|
431
439
|
function buildModelUsageTableLines(rows, totals, selectedModelId) {
|
|
432
|
-
return
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
color: "gray"
|
|
443
|
-
},
|
|
444
|
-
{
|
|
445
|
-
key: "model-header-border",
|
|
446
|
-
text: buildTableBorder(MODEL_TABLE_COLUMNS, "├", "┼", "┤"),
|
|
447
|
-
color: "gray"
|
|
448
|
-
},
|
|
449
|
-
...rows.map((row) => {
|
|
450
|
-
const isSelected = selectedModelId === row.modelId;
|
|
451
|
-
return {
|
|
452
|
-
key: `model-row:${row.modelId}`,
|
|
453
|
-
text: formatModelUsageTableRow(row.modelId, row.totals),
|
|
454
|
-
inverse: isSelected,
|
|
455
|
-
color: isSelected ? "cyan" : undefined
|
|
456
|
-
};
|
|
457
|
-
}),
|
|
458
|
-
{
|
|
459
|
-
key: "model-total-border",
|
|
460
|
-
text: buildTableBorder(MODEL_TABLE_COLUMNS, "├", "┼", "┤"),
|
|
461
|
-
color: "gray"
|
|
462
|
-
},
|
|
463
|
-
{
|
|
440
|
+
return buildTextTableLines({
|
|
441
|
+
title: "Model usage",
|
|
442
|
+
lineKeyPrefix: "model",
|
|
443
|
+
headers: MODEL_TABLE_HEADERS,
|
|
444
|
+
rows: rows.map((row) => ({
|
|
445
|
+
key: `model-row:${row.modelId}`,
|
|
446
|
+
cells: formatModelUsageTableCells(row.modelId, row.totals)
|
|
447
|
+
})),
|
|
448
|
+
selectedRowKey: selectedModelId ? `model-row:${selectedModelId}` : undefined,
|
|
449
|
+
totalRow: {
|
|
464
450
|
key: "model-total",
|
|
465
|
-
|
|
466
|
-
color: "cyan"
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
key: "model-bottom-border",
|
|
470
|
-
text: buildTableBorder(MODEL_TABLE_COLUMNS, "└", "┴", "┘"),
|
|
471
|
-
color: "gray"
|
|
451
|
+
cells: formatModelUsageTableCells("TOTAL", totals)
|
|
472
452
|
}
|
|
473
|
-
|
|
453
|
+
});
|
|
474
454
|
}
|
|
475
455
|
function SelectionDetailsPanel(props) {
|
|
476
456
|
if (props.providerState.status !== "ready") {
|
|
@@ -697,16 +677,16 @@ function formatEventRange(firstEventUtcIso, lastEventUtcIso) {
|
|
|
697
677
|
function pad(value, length) {
|
|
698
678
|
return value.length >= length ? value.slice(0, length) : value.padEnd(length);
|
|
699
679
|
}
|
|
700
|
-
function
|
|
680
|
+
function formatModelUsageTableCells(modelId, totals) {
|
|
701
681
|
const displayModelId = modelId === "unknown" ? "-" : modelId;
|
|
702
|
-
return
|
|
682
|
+
return [
|
|
703
683
|
displayModelId,
|
|
704
684
|
formatCompactTokenCount(totals.inputTokens),
|
|
705
685
|
formatCompactTokenCount(totals.outputTokens),
|
|
706
686
|
formatCompactCacheTokens(totals, totals.cacheReadInputTokens),
|
|
707
687
|
formatCompactCacheTokens(totals, totals.cacheWriteInputTokens),
|
|
708
688
|
formatUsageUsd(totals, modelId)
|
|
709
|
-
]
|
|
689
|
+
];
|
|
710
690
|
}
|
|
711
691
|
function UsageBreakdownLines(props) {
|
|
712
692
|
const { totals } = props;
|
|
@@ -210,8 +210,8 @@ async function collectAntigravityQuotaFromLocalRpc() {
|
|
|
210
210
|
return {
|
|
211
211
|
entries: parseAntigravityQuotaEntries(quota),
|
|
212
212
|
fetchedAt: Date.now(),
|
|
213
|
-
planType: status
|
|
214
|
-
userIdHash:
|
|
213
|
+
planType: parseAntigravityPlanType(status),
|
|
214
|
+
userIdHash: parseAntigravityUserIdHash(status)
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
|
|
@@ -384,6 +384,16 @@ export function parseAntigravityQuotaEntries(payload) {
|
|
|
384
384
|
});
|
|
385
385
|
});
|
|
386
386
|
}
|
|
387
|
+
export function parseAntigravityPlanType(payload) {
|
|
388
|
+
const planName = payload.response?.userStatus?.planStatus?.planInfo?.planName;
|
|
389
|
+
return typeof planName === "string" && planName ? planName : "unknown";
|
|
390
|
+
}
|
|
391
|
+
export function parseAntigravityUserIdHash(payload) {
|
|
392
|
+
const email = payload.response?.userStatus?.email;
|
|
393
|
+
return typeof email === "string" && email
|
|
394
|
+
? createHash("md5").update(email).digest("hex")
|
|
395
|
+
: null;
|
|
396
|
+
}
|
|
387
397
|
function resolveQuotaGroupModelIds(text) {
|
|
388
398
|
return (QUOTA_MODEL_GROUPS.find(({ pattern }) => pattern.test(text.toLowerCase()))?.models ?? []);
|
|
389
399
|
}
|
|
@@ -125,7 +125,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
125
125
|
warnings.push(`Collapsed ${parsedEvents.duplicateUsageKeys} duplicate Claude usage event(s) by request/message key.`);
|
|
126
126
|
}
|
|
127
127
|
if (options.verbose && parsedEvents.duplicateUsageKeyCollisions > 0) {
|
|
128
|
-
warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage;
|
|
128
|
+
warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage; merged same-key rows by per-field maxima to avoid double-counting cumulative snapshots.`);
|
|
129
129
|
}
|
|
130
130
|
if (options.verbose && parsedEvents.duplicateUnkeyedEvents > 0) {
|
|
131
131
|
warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} duplicate unkeyed Claude usage event(s) by usage signature.`);
|
|
@@ -455,6 +455,7 @@ async function parseSessionFile(filePath, sessionsRoot) {
|
|
|
455
455
|
usageSignature,
|
|
456
456
|
timestampMs: eventTimeMs,
|
|
457
457
|
modelId,
|
|
458
|
+
usage: normalizedUsage,
|
|
458
459
|
totals: usageToTotals(modelId, normalizedUsage),
|
|
459
460
|
rateLimits
|
|
460
461
|
});
|
|
@@ -565,8 +566,11 @@ function buildUsageEventKey(payloadObject, message) {
|
|
|
565
566
|
return `${sessionId}|${requestId || messageId}`;
|
|
566
567
|
}
|
|
567
568
|
function buildUsageSignature(payloadObject, modelId, usage) {
|
|
569
|
+
return buildUsageSignatureFromParts(String(payloadObject.sessionId ?? ""), modelId, usage);
|
|
570
|
+
}
|
|
571
|
+
function buildUsageSignatureFromParts(sessionId, modelId, usage) {
|
|
568
572
|
return [
|
|
569
|
-
|
|
573
|
+
sessionId,
|
|
570
574
|
modelId,
|
|
571
575
|
usage.inputTokens,
|
|
572
576
|
usage.cacheCreationInputTokens,
|
|
@@ -597,9 +601,7 @@ function recordParsedUsageEvent(parsedEvents, event) {
|
|
|
597
601
|
if (previous.usageSignature !== event.usageSignature) {
|
|
598
602
|
parsedEvents.duplicateUsageKeyCollisions += 1;
|
|
599
603
|
}
|
|
600
|
-
|
|
601
|
-
parsedEvents.keyedEvents.set(event.usageKey, event);
|
|
602
|
-
}
|
|
604
|
+
parsedEvents.keyedEvents.set(event.usageKey, mergeParsedUsageEvents(previous, event));
|
|
603
605
|
return;
|
|
604
606
|
}
|
|
605
607
|
const previous = parsedEvents.unkeyedEvents.get(event.usageSignature);
|
|
@@ -608,18 +610,57 @@ function recordParsedUsageEvent(parsedEvents, event) {
|
|
|
608
610
|
return;
|
|
609
611
|
}
|
|
610
612
|
parsedEvents.duplicateUnkeyedEvents += 1;
|
|
611
|
-
if (
|
|
613
|
+
if (normalizeTimestamp(event.timestampMs) > normalizeTimestamp(previous.timestampMs)) {
|
|
612
614
|
parsedEvents.unkeyedEvents.set(event.usageSignature, event);
|
|
613
615
|
}
|
|
614
616
|
}
|
|
615
|
-
function
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
function mergeParsedUsageEvents(previous, next) {
|
|
618
|
+
const mergedUsage = mergeClaudeUsage(previous.usage, next.usage);
|
|
619
|
+
const modelId = selectMergedEventModelId(previous, next);
|
|
620
|
+
const latestEvent = normalizeTimestamp(next.timestampMs) >= normalizeTimestamp(previous.timestampMs) ? next : previous;
|
|
621
|
+
const sessionId = extractUsageKeySessionId(previous.usageKey) || extractUsageKeySessionId(next.usageKey);
|
|
622
|
+
return {
|
|
623
|
+
entrypoint: latestEvent.entrypoint || previous.entrypoint || next.entrypoint,
|
|
624
|
+
usageKey: previous.usageKey ?? next.usageKey,
|
|
625
|
+
usageSignature: buildUsageSignatureFromParts(sessionId, modelId, mergedUsage),
|
|
626
|
+
timestampMs: Math.max(normalizeTimestamp(previous.timestampMs), normalizeTimestamp(next.timestampMs)),
|
|
627
|
+
modelId,
|
|
628
|
+
usage: mergedUsage,
|
|
629
|
+
totals: usageToTotals(modelId, mergedUsage),
|
|
630
|
+
rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function mergeClaudeUsage(previous, next) {
|
|
634
|
+
return {
|
|
635
|
+
inputTokens: Math.max(previous.inputTokens, next.inputTokens),
|
|
636
|
+
cacheReadInputTokens: Math.max(previous.cacheReadInputTokens, next.cacheReadInputTokens),
|
|
637
|
+
cacheCreationInputTokens: Math.max(previous.cacheCreationInputTokens, next.cacheCreationInputTokens),
|
|
638
|
+
cacheCreation5mInputTokens: Math.max(previous.cacheCreation5mInputTokens, next.cacheCreation5mInputTokens),
|
|
639
|
+
cacheCreation1hInputTokens: Math.max(previous.cacheCreation1hInputTokens, next.cacheCreation1hInputTokens),
|
|
640
|
+
outputTokens: Math.max(previous.outputTokens, next.outputTokens),
|
|
641
|
+
inferenceGeo: next.inferenceGeo || previous.inferenceGeo
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function selectMergedEventModelId(previous, next) {
|
|
645
|
+
if (previous.modelId === next.modelId) {
|
|
646
|
+
return previous.modelId;
|
|
647
|
+
}
|
|
648
|
+
if (isInternalClaudeModel(previous.modelId) && !isInternalClaudeModel(next.modelId)) {
|
|
649
|
+
return next.modelId;
|
|
650
|
+
}
|
|
651
|
+
if (isInternalClaudeModel(next.modelId) && !isInternalClaudeModel(previous.modelId)) {
|
|
652
|
+
return previous.modelId;
|
|
618
653
|
}
|
|
619
|
-
|
|
620
|
-
|
|
654
|
+
return next.totals.estimatedCredits >= previous.totals.estimatedCredits
|
|
655
|
+
? next.modelId
|
|
656
|
+
: previous.modelId;
|
|
657
|
+
}
|
|
658
|
+
function extractUsageKeySessionId(usageKey) {
|
|
659
|
+
if (!usageKey) {
|
|
660
|
+
return "";
|
|
621
661
|
}
|
|
622
|
-
|
|
662
|
+
const separatorIndex = usageKey.indexOf("|");
|
|
663
|
+
return separatorIndex >= 0 ? usageKey.slice(0, separatorIndex) : usageKey;
|
|
623
664
|
}
|
|
624
665
|
function normalizeTimestamp(value) {
|
|
625
666
|
return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
|
|
@@ -696,7 +737,7 @@ async function buildLiveLimitWindows(options) {
|
|
|
696
737
|
if (snapshots.length === 0) {
|
|
697
738
|
traceClaude(options.traceLogger, "No live usage snapshots matched the expected /usage format.");
|
|
698
739
|
}
|
|
699
|
-
const resolvedPlanType = subscriptionType
|
|
740
|
+
const resolvedPlanType = resolveClaudeLivePlanType(subscriptionType, snapshots);
|
|
700
741
|
traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
|
|
701
742
|
const primaryLimitWindows = snapshots
|
|
702
743
|
.filter((snapshot) => snapshot.scope === "primary")
|
|
@@ -707,14 +748,15 @@ async function buildLiveLimitWindows(options) {
|
|
|
707
748
|
for (let index = 0; index < snapshots.length; index += 1) {
|
|
708
749
|
const snapshot = snapshots[index];
|
|
709
750
|
const row = snapshot.scope === "primary"
|
|
710
|
-
? primaryLimitWindows.
|
|
711
|
-
: secondaryLimitWindows.
|
|
751
|
+
? primaryLimitWindows.find((window) => window.limitId === snapshot.limitId)
|
|
752
|
+
: secondaryLimitWindows.find((window) => window.limitId === snapshot.limitId);
|
|
712
753
|
if (!row) {
|
|
713
754
|
continue;
|
|
714
755
|
}
|
|
715
756
|
traceClaude(options.traceLogger, [
|
|
716
757
|
`Live window ${snapshot.scope}/${snapshot.label}:`,
|
|
717
758
|
`used=${snapshot.usedPercent}%`,
|
|
759
|
+
`limit=${snapshot.limitId}`,
|
|
718
760
|
`range=${row.startTimeUtcIso}->${row.endTimeUtcIso}`,
|
|
719
761
|
`matchedEvents=${row.eventCount}`,
|
|
720
762
|
`input=${row.totals.inputTokens}`,
|
|
@@ -728,6 +770,12 @@ async function buildLiveLimitWindows(options) {
|
|
|
728
770
|
secondaryLimitWindows
|
|
729
771
|
};
|
|
730
772
|
}
|
|
773
|
+
function resolveClaudeLivePlanType(subscriptionType, snapshots) {
|
|
774
|
+
if (snapshots.some((snapshot) => snapshot.modelScope === "sonnet-only")) {
|
|
775
|
+
return "team_premium";
|
|
776
|
+
}
|
|
777
|
+
return subscriptionType || "live";
|
|
778
|
+
}
|
|
731
779
|
async function readClaudeSubscriptionType(root, override, traceLogger) {
|
|
732
780
|
const output = await readClaudeAuthStatusOutput(root, override, traceLogger);
|
|
733
781
|
const subscriptionType = parseClaudeSubscriptionType(output);
|
|
@@ -992,26 +1040,32 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
992
1040
|
for (const line of normalizedOutput.split(/\r?\n/)) {
|
|
993
1041
|
const match = line
|
|
994
1042
|
.trim()
|
|
995
|
-
.match(/^Current\s+(session|week)(?:\s+\([^)]
|
|
1043
|
+
.match(/^Current\s+(session|week)(?:\s+\(([^)]+)\))?:\s+(\d+)%\s+used\b.*?\bresets\s+(.+)$/i);
|
|
996
1044
|
if (!match) {
|
|
997
1045
|
continue;
|
|
998
1046
|
}
|
|
999
1047
|
const label = match[1].toLowerCase() === "session" ? "session" : "week";
|
|
1000
|
-
const
|
|
1048
|
+
const windowQualifier = (match[2] ?? "").trim().toLowerCase();
|
|
1049
|
+
const usedPercent = Number(match[3]);
|
|
1001
1050
|
const windowMinutes = label === "session" ? CLAUDE_SESSION_WINDOW_MINUTES : CLAUDE_WEEK_WINDOW_MINUTES;
|
|
1002
|
-
const resetsAtMs = parseResetTimestampUtc(match[
|
|
1051
|
+
const resetsAtMs = parseResetTimestampUtc(match[4], now.getTime(), windowMinutes);
|
|
1003
1052
|
if (!Number.isFinite(usedPercent) || !resetsAtMs) {
|
|
1004
1053
|
continue;
|
|
1005
1054
|
}
|
|
1006
|
-
|
|
1055
|
+
const isSonnetOnlyWeek = label === "week" && windowQualifier === "sonnet only";
|
|
1056
|
+
const limitId = isSonnetOnlyWeek ? "current-week-sonnet-only" : `current-${label}`;
|
|
1057
|
+
snapshots.set(limitId, {
|
|
1007
1058
|
scope: label === "session" ? "primary" : "secondary",
|
|
1008
1059
|
label,
|
|
1060
|
+
limitId,
|
|
1061
|
+
modelScope: isSonnetOnlyWeek ? "sonnet-only" : "all-models",
|
|
1062
|
+
modelType: isSonnetOnlyWeek ? "sonnet only" : undefined,
|
|
1009
1063
|
usedPercent,
|
|
1010
1064
|
resetsAtMs,
|
|
1011
1065
|
windowMinutes
|
|
1012
1066
|
});
|
|
1013
1067
|
}
|
|
1014
|
-
return [...snapshots.values()].sort((left, right) => left.windowMinutes - right.windowMinutes);
|
|
1068
|
+
return [...snapshots.values()].sort((left, right) => left.windowMinutes - right.windowMinutes || left.limitId.localeCompare(right.limitId));
|
|
1015
1069
|
}
|
|
1016
1070
|
function parseResetTimestampUtc(value, nowMs, windowMinutes) {
|
|
1017
1071
|
const match = value
|
|
@@ -1062,7 +1116,8 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1062
1116
|
const startTimeMs = snapshot.resetsAtMs - snapshot.windowMinutes * 60000;
|
|
1063
1117
|
const inWindowEvents = selectedEvents.filter((event) => Number.isFinite(event.timestampMs) &&
|
|
1064
1118
|
event.timestampMs >= startTimeMs &&
|
|
1065
|
-
event.timestampMs < snapshot.resetsAtMs
|
|
1119
|
+
event.timestampMs < snapshot.resetsAtMs &&
|
|
1120
|
+
matchesClaudeLiveSnapshotModelScope(snapshot, event.modelId));
|
|
1066
1121
|
const totals = sumUsageTotals(inWindowEvents.map((event) => event.totals));
|
|
1067
1122
|
const fallbackLastSeenMs = Math.min(now.getTime(), snapshot.resetsAtMs);
|
|
1068
1123
|
const firstSeenMs = inWindowEvents.reduce((minimum, event) => Math.min(minimum, event.timestampMs), Number.POSITIVE_INFINITY);
|
|
@@ -1070,7 +1125,8 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1070
1125
|
return {
|
|
1071
1126
|
scope: snapshot.scope,
|
|
1072
1127
|
planType,
|
|
1073
|
-
limitId:
|
|
1128
|
+
limitId: snapshot.limitId,
|
|
1129
|
+
modelType: snapshot.modelType,
|
|
1074
1130
|
windowMinutes: snapshot.windowMinutes,
|
|
1075
1131
|
startTimeUtcIso: toUtcIso(startTimeMs),
|
|
1076
1132
|
endTimeUtcIso: toUtcIso(snapshot.resetsAtMs),
|
|
@@ -1083,6 +1139,12 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1083
1139
|
eventCount: totals.eventCount
|
|
1084
1140
|
};
|
|
1085
1141
|
}
|
|
1142
|
+
function matchesClaudeLiveSnapshotModelScope(snapshot, modelId) {
|
|
1143
|
+
if (snapshot.modelScope !== "sonnet-only") {
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
return modelId.toLowerCase().includes("sonnet");
|
|
1147
|
+
}
|
|
1086
1148
|
function buildModelUsageRowsForEvents(events) {
|
|
1087
1149
|
const byModel = new Map();
|
|
1088
1150
|
for (const event of events) {
|
|
@@ -4,6 +4,7 @@ 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
8
|
let versionCache = null;
|
|
8
9
|
export async function reportAnonymousUsage(statsList) {
|
|
9
10
|
const payload = await buildAnonymousUsagePayload(statsList);
|
|
@@ -15,10 +16,12 @@ export async function reportAnonymousUsage(statsList) {
|
|
|
15
16
|
export async function buildAnonymousUsageReports(statsList) {
|
|
16
17
|
const letmecodeVersion = await readLetmecodeVersion();
|
|
17
18
|
return statsList.flatMap((stats) => {
|
|
18
|
-
if (!stats.analytics?.userIdHash) {
|
|
19
|
+
if (!stats.analytics?.userIdHash || stats.providerId === "antigravity") {
|
|
19
20
|
return [];
|
|
20
21
|
}
|
|
21
|
-
return [...stats.primaryLimitWindows, ...stats.secondaryLimitWindows]
|
|
22
|
+
return [...stats.primaryLimitWindows, ...stats.secondaryLimitWindows]
|
|
23
|
+
.filter((window) => shouldReportUsageWindow(window))
|
|
24
|
+
.map((window) => buildAnonymousUsageReport(stats, window, letmecodeVersion));
|
|
22
25
|
});
|
|
23
26
|
}
|
|
24
27
|
export async function buildAnonymousUsagePayload(statsList) {
|
|
@@ -46,6 +49,9 @@ function resolveReportModelType(stats, window) {
|
|
|
46
49
|
if (stats.providerId === "antigravity") {
|
|
47
50
|
return resolveAntigravityReportModelType(stats, window);
|
|
48
51
|
}
|
|
52
|
+
if (window.modelType) {
|
|
53
|
+
return truncateSchemaString(window.modelType, 128);
|
|
54
|
+
}
|
|
49
55
|
if (window.limitId && window.limitId !== "unknown") {
|
|
50
56
|
return truncateSchemaString(window.limitId, 128);
|
|
51
57
|
}
|
|
@@ -86,6 +92,9 @@ function resolveReportedUsedPercents(window) {
|
|
|
86
92
|
}
|
|
87
93
|
return clampPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
88
94
|
}
|
|
95
|
+
function shouldReportUsageWindow(window) {
|
|
96
|
+
return resolveReportedUsedPercents(window) >= MIN_REPORTED_USED_PERCENTS;
|
|
97
|
+
}
|
|
89
98
|
function clampPercent(value) {
|
|
90
99
|
if (!Number.isFinite(value)) {
|
|
91
100
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Provider-based terminal usage dashboard for LetMeCode.",
|
|
5
5
|
"author": "devforth.io",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"packageManager": "pnpm@10.28.2",
|
|
7
8
|
"type": "commonjs",
|
|
8
9
|
"bin": {
|
|
9
10
|
"letmecode": "./bin/letmecode.js"
|
|
@@ -20,6 +21,16 @@
|
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public"
|
|
22
23
|
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
26
|
+
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
27
|
+
"prepack": "npm run build",
|
|
28
|
+
"prestart": "npm run build",
|
|
29
|
+
"start": "node ./bin/letmecode.js",
|
|
30
|
+
"pretest": "npm run build",
|
|
31
|
+
"smoke": "node ./bin/letmecode.js",
|
|
32
|
+
"test": "node --test ink-app/test/*.test.mjs"
|
|
33
|
+
},
|
|
23
34
|
"keywords": [
|
|
24
35
|
"cli",
|
|
25
36
|
"ink",
|
|
@@ -35,14 +46,5 @@
|
|
|
35
46
|
"@types/node": "^24.0.7",
|
|
36
47
|
"@types/react": "^18.3.24",
|
|
37
48
|
"typescript": "^5.8.3"
|
|
38
|
-
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
41
|
-
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
42
|
-
"prestart": "npm run build",
|
|
43
|
-
"start": "node ./bin/letmecode.js",
|
|
44
|
-
"pretest": "npm run build",
|
|
45
|
-
"smoke": "node ./bin/letmecode.js",
|
|
46
|
-
"test": "node --test ink-app/test/*.test.mjs"
|
|
47
49
|
}
|
|
48
|
-
}
|
|
50
|
+
}
|