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.
@@ -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 LIMIT_TABLE_COLUMNS = [
23
- { header: "Plan", width: 5 },
24
- { header: "Window", width: 6 },
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: "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" })] }));
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 bodyLines = [
294
- ...buildLimitWindowTableLines("primary", "Primary limits", props.stats.primaryLimitWindows, props.selectedRowKey),
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 bodyLines = buildModelUsageTableLines(props.stats.modelUsage, totals, props.selectedModelId);
306
- return (_jsx(ScrollableLineViewport, { bodyLines: bodyLines, selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
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 bodyLines = buildDailyUsageTableLines(props.stats.dayUsage, props.selectedDayKey);
313
- return (_jsx(ScrollableLineViewport, { bodyLines: bodyLines, selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
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 buildTableBorder(columns, left, middle, right) {
334
- return `${left}${columns.map((column) => "─".repeat(column.width + 2)).join(middle)}${right}`;
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(columns, cells) {
337
- return `│${columns.map((column, index) => ` ${pad(cells[index] ?? "", column.width)} `).join("│")}│`;
316
+ function buildTableRow(table, cells) {
317
+ return `│${table.widths.map((width, index) => ` ${pad(cells[index] ?? "", width)} `).join("│")}│`;
338
318
  }
339
- function buildLimitWindowTableLines(scope, title, windows, selectedRowKey) {
340
- if (windows.length === 0) {
341
- return [
342
- { key: `${scope}-title`, text: title, bold: true },
343
- { key: `${scope}-empty`, text: "No windows found.", color: "gray" }
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
- return [
347
- { key: `${scope}-title`, text: title, bold: true },
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: `${scope}-top-border`,
350
- text: buildTableBorder(LIMIT_TABLE_COLUMNS, "┌", "┬", "┐"),
335
+ key: `${options.lineKeyPrefix}-top-border`,
336
+ text: buildTableBorder(table, "┌", "┬", "┐"),
351
337
  color: "gray"
352
338
  },
353
339
  {
354
- key: `${scope}-header`,
355
- text: buildTableRow(LIMIT_TABLE_COLUMNS, LIMIT_TABLE_COLUMNS.map((column) => column.header)),
340
+ key: `${options.lineKeyPrefix}-header`,
341
+ text: buildTableRow(table, table.headers),
356
342
  color: "gray"
357
343
  },
358
344
  {
359
- key: `${scope}-header-border`,
360
- text: buildTableBorder(LIMIT_TABLE_COLUMNS, "├", "┼", "┤"),
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
- function buildDailyUsageTableLines(rows, selectedDayKey) {
390
- return [
391
- { key: "daily-title", text: "Daily usage", bold: true },
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
- key: "daily-bottom-border",
426
- text: buildTableBorder(DAILY_TABLE_COLUMNS, "", "", ""),
427
- color: "gray"
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
- { key: "model-title", text: "Model usage", bold: true },
434
- {
435
- key: "model-top-border",
436
- text: buildTableBorder(MODEL_TABLE_COLUMNS, "┌", "┬", "┐"),
437
- color: "gray"
438
- },
439
- {
440
- key: "model-header",
441
- text: buildTableRow(MODEL_TABLE_COLUMNS, MODEL_TABLE_COLUMNS.map((column) => column.header)),
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
- text: formatModelUsageTableRow("TOTAL", totals),
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 formatModelUsageTableRow(modelId, totals) {
680
+ function formatModelUsageTableCells(modelId, totals) {
701
681
  const displayModelId = modelId === "unknown" ? "-" : modelId;
702
- return buildTableRow(MODEL_TABLE_COLUMNS, [
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.userStatus.planStatus.planInfo.planName ?? "unknown",
214
- userIdHash: createHash("md5").update(status.userStatus.email).digest("hex")
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; keeping the highest-cost/latest event per key.`);
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
- String(payloadObject.sessionId ?? ""),
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
- if (shouldReplaceUsageEvent(previous, event)) {
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 (shouldReplaceUsageEvent(previous, event)) {
613
+ if (normalizeTimestamp(event.timestampMs) > normalizeTimestamp(previous.timestampMs)) {
612
614
  parsedEvents.unkeyedEvents.set(event.usageSignature, event);
613
615
  }
614
616
  }
615
- function shouldReplaceUsageEvent(previous, next) {
616
- if (next.totals.estimatedCredits > previous.totals.estimatedCredits) {
617
- return true;
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
- if (next.totals.estimatedCredits === previous.totals.estimatedCredits) {
620
- return normalizeTimestamp(next.timestampMs) > normalizeTimestamp(previous.timestampMs);
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
- return false;
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 || "live";
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.filter((window) => window.limitId === `current-${snapshot.label}`)[0]
711
- : secondaryLimitWindows.filter((window) => window.limitId === `current-${snapshot.label}`)[0];
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+\([^)]+\))?:\s+(\d+)%\s+used\b.*?\bresets\s+(.+)$/i);
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 usedPercent = Number(match[2]);
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[3], now.getTime(), windowMinutes);
1051
+ const resetsAtMs = parseResetTimestampUtc(match[4], now.getTime(), windowMinutes);
1003
1052
  if (!Number.isFinite(usedPercent) || !resetsAtMs) {
1004
1053
  continue;
1005
1054
  }
1006
- snapshots.set(label, {
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: `current-${snapshot.label}`,
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].map((window) => buildAnonymousUsageReport(stats, window, letmecodeVersion));
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.13",
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
+ }