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.
@@ -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
  }
@@ -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 || "live";
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.filter((window) => window.limitId === `current-${snapshot.label}`)[0]
711
- : secondaryLimitWindows.filter((window) => window.limitId === `current-${snapshot.label}`)[0];
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+\([^)]+\))?:\s+(\d+)%\s+used\b.*?\bresets\s+(.+)$/i);
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 usedPercent = Number(match[2]);
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[3], now.getTime(), windowMinutes);
1010
+ const resetsAtMs = parseResetTimestampUtc(match[4], now.getTime(), windowMinutes);
1003
1011
  if (!Number.isFinite(usedPercent) || !resetsAtMs) {
1004
1012
  continue;
1005
1013
  }
1006
- snapshots.set(label, {
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: `current-${snapshot.label}`,
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].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.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
+ }