letmecode 0.1.13 → 0.1.15
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 +31 -10
- 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
|
}
|
|
@@ -696,7 +696,7 @@ async function buildLiveLimitWindows(options) {
|
|
|
696
696
|
if (snapshots.length === 0) {
|
|
697
697
|
traceClaude(options.traceLogger, "No live usage snapshots matched the expected /usage format.");
|
|
698
698
|
}
|
|
699
|
-
const resolvedPlanType = subscriptionType
|
|
699
|
+
const resolvedPlanType = resolveClaudeLivePlanType(subscriptionType, snapshots);
|
|
700
700
|
traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
|
|
701
701
|
const primaryLimitWindows = snapshots
|
|
702
702
|
.filter((snapshot) => snapshot.scope === "primary")
|
|
@@ -707,14 +707,15 @@ async function buildLiveLimitWindows(options) {
|
|
|
707
707
|
for (let index = 0; index < snapshots.length; index += 1) {
|
|
708
708
|
const snapshot = snapshots[index];
|
|
709
709
|
const row = snapshot.scope === "primary"
|
|
710
|
-
? primaryLimitWindows.
|
|
711
|
-
: secondaryLimitWindows.
|
|
710
|
+
? primaryLimitWindows.find((window) => window.limitId === snapshot.limitId)
|
|
711
|
+
: secondaryLimitWindows.find((window) => window.limitId === snapshot.limitId);
|
|
712
712
|
if (!row) {
|
|
713
713
|
continue;
|
|
714
714
|
}
|
|
715
715
|
traceClaude(options.traceLogger, [
|
|
716
716
|
`Live window ${snapshot.scope}/${snapshot.label}:`,
|
|
717
717
|
`used=${snapshot.usedPercent}%`,
|
|
718
|
+
`limit=${snapshot.limitId}`,
|
|
718
719
|
`range=${row.startTimeUtcIso}->${row.endTimeUtcIso}`,
|
|
719
720
|
`matchedEvents=${row.eventCount}`,
|
|
720
721
|
`input=${row.totals.inputTokens}`,
|
|
@@ -728,6 +729,12 @@ async function buildLiveLimitWindows(options) {
|
|
|
728
729
|
secondaryLimitWindows
|
|
729
730
|
};
|
|
730
731
|
}
|
|
732
|
+
function resolveClaudeLivePlanType(subscriptionType, snapshots) {
|
|
733
|
+
if (snapshots.some((snapshot) => snapshot.modelScope === "sonnet-only")) {
|
|
734
|
+
return "team_premium";
|
|
735
|
+
}
|
|
736
|
+
return subscriptionType || "live";
|
|
737
|
+
}
|
|
731
738
|
async function readClaudeSubscriptionType(root, override, traceLogger) {
|
|
732
739
|
const output = await readClaudeAuthStatusOutput(root, override, traceLogger);
|
|
733
740
|
const subscriptionType = parseClaudeSubscriptionType(output);
|
|
@@ -992,26 +999,32 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
992
999
|
for (const line of normalizedOutput.split(/\r?\n/)) {
|
|
993
1000
|
const match = line
|
|
994
1001
|
.trim()
|
|
995
|
-
.match(/^Current\s+(session|week)(?:\s+\([^)]
|
|
1002
|
+
.match(/^Current\s+(session|week)(?:\s+\(([^)]+)\))?:\s+(\d+)%\s+used\b.*?\bresets\s+(.+)$/i);
|
|
996
1003
|
if (!match) {
|
|
997
1004
|
continue;
|
|
998
1005
|
}
|
|
999
1006
|
const label = match[1].toLowerCase() === "session" ? "session" : "week";
|
|
1000
|
-
const
|
|
1007
|
+
const windowQualifier = (match[2] ?? "").trim().toLowerCase();
|
|
1008
|
+
const usedPercent = Number(match[3]);
|
|
1001
1009
|
const windowMinutes = label === "session" ? CLAUDE_SESSION_WINDOW_MINUTES : CLAUDE_WEEK_WINDOW_MINUTES;
|
|
1002
|
-
const resetsAtMs = parseResetTimestampUtc(match[
|
|
1010
|
+
const resetsAtMs = parseResetTimestampUtc(match[4], now.getTime(), windowMinutes);
|
|
1003
1011
|
if (!Number.isFinite(usedPercent) || !resetsAtMs) {
|
|
1004
1012
|
continue;
|
|
1005
1013
|
}
|
|
1006
|
-
|
|
1014
|
+
const isSonnetOnlyWeek = label === "week" && windowQualifier === "sonnet only";
|
|
1015
|
+
const limitId = isSonnetOnlyWeek ? "current-week-sonnet-only" : `current-${label}`;
|
|
1016
|
+
snapshots.set(limitId, {
|
|
1007
1017
|
scope: label === "session" ? "primary" : "secondary",
|
|
1008
1018
|
label,
|
|
1019
|
+
limitId,
|
|
1020
|
+
modelScope: isSonnetOnlyWeek ? "sonnet-only" : "all-models",
|
|
1021
|
+
modelType: isSonnetOnlyWeek ? "sonnet only" : undefined,
|
|
1009
1022
|
usedPercent,
|
|
1010
1023
|
resetsAtMs,
|
|
1011
1024
|
windowMinutes
|
|
1012
1025
|
});
|
|
1013
1026
|
}
|
|
1014
|
-
return [...snapshots.values()].sort((left, right) => left.windowMinutes - right.windowMinutes);
|
|
1027
|
+
return [...snapshots.values()].sort((left, right) => left.windowMinutes - right.windowMinutes || left.limitId.localeCompare(right.limitId));
|
|
1015
1028
|
}
|
|
1016
1029
|
function parseResetTimestampUtc(value, nowMs, windowMinutes) {
|
|
1017
1030
|
const match = value
|
|
@@ -1062,7 +1075,8 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1062
1075
|
const startTimeMs = snapshot.resetsAtMs - snapshot.windowMinutes * 60000;
|
|
1063
1076
|
const inWindowEvents = selectedEvents.filter((event) => Number.isFinite(event.timestampMs) &&
|
|
1064
1077
|
event.timestampMs >= startTimeMs &&
|
|
1065
|
-
event.timestampMs < snapshot.resetsAtMs
|
|
1078
|
+
event.timestampMs < snapshot.resetsAtMs &&
|
|
1079
|
+
matchesClaudeLiveSnapshotModelScope(snapshot, event.modelId));
|
|
1066
1080
|
const totals = sumUsageTotals(inWindowEvents.map((event) => event.totals));
|
|
1067
1081
|
const fallbackLastSeenMs = Math.min(now.getTime(), snapshot.resetsAtMs);
|
|
1068
1082
|
const firstSeenMs = inWindowEvents.reduce((minimum, event) => Math.min(minimum, event.timestampMs), Number.POSITIVE_INFINITY);
|
|
@@ -1070,7 +1084,8 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1070
1084
|
return {
|
|
1071
1085
|
scope: snapshot.scope,
|
|
1072
1086
|
planType,
|
|
1073
|
-
limitId:
|
|
1087
|
+
limitId: snapshot.limitId,
|
|
1088
|
+
modelType: snapshot.modelType,
|
|
1074
1089
|
windowMinutes: snapshot.windowMinutes,
|
|
1075
1090
|
startTimeUtcIso: toUtcIso(startTimeMs),
|
|
1076
1091
|
endTimeUtcIso: toUtcIso(snapshot.resetsAtMs),
|
|
@@ -1083,6 +1098,12 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1083
1098
|
eventCount: totals.eventCount
|
|
1084
1099
|
};
|
|
1085
1100
|
}
|
|
1101
|
+
function matchesClaudeLiveSnapshotModelScope(snapshot, modelId) {
|
|
1102
|
+
if (snapshot.modelScope !== "sonnet-only") {
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
return modelId.toLowerCase().includes("sonnet");
|
|
1106
|
+
}
|
|
1086
1107
|
function buildModelUsageRowsForEvents(events) {
|
|
1087
1108
|
const byModel = new Map();
|
|
1088
1109
|
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.15",
|
|
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
|
+
}
|