letmecode 0.1.22 → 0.1.24

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Devforth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,32 +1,51 @@
1
- # letmecode - Discover your detailed agent ussage Codex | Claude
1
+ # LetMeCode
2
2
 
3
- Ussage:
3
+ Terminal AI real-money value usage dashboard for Codex, Claude, Copilot, and Antigravity.
4
+
5
+ See your real $ usage data, inspect limits, see daily activity, and model-level token totals in a terminal UI.
6
+
7
+ ## Quick start
4
8
 
5
9
  ```bash
6
10
  npx -y letmecode@latest
7
11
  ```
8
12
 
13
+ ## Privacy and anonymous reporting
9
14
 
10
- Preview:
11
-
12
- <img width="2301" height="1397" alt="image" src="https://github.com/user-attachments/assets/c37d6847-9926-4977-8592-5cab8346a86f" />
15
+ By default, `letmecode` sends an anonymous usage summary which powers [aggregated real-world plans comparison](https://devforth.io/agents-for-code/).
13
16
 
17
+ The report includes aggregated limit-window percentages, token counts, plan/window metadata, the `letmecode` version, and a hashed user identifier when a provider exposes one. It does not send prompt content, usernames, email addresses, company names, or other directly identifiable personal information.
14
18
 
19
+ To disable anonymous usage reporting:
15
20
 
21
+ ```bash
22
+ npx -y letmecode@latest -- --no-usage
23
+ ```
16
24
 
17
- ## Advanced ussage
25
+ ## Options
18
26
 
27
+ ```bash
28
+ npx -y letmecode@latest -- --help
29
+ npx -y letmecode@latest -- --log-to ./letmecode.log
19
30
  ```
20
- npx -y letmecode -- -h
21
- npx -y letmecode -- --log-to log.txt
22
- ```
23
31
 
24
- `--log-to` now records Claude binary discovery, session-root selection, parsed transcript file summaries, entrypoint matching, raw `/usage` output, and live-window event matching so zero-token windows are diagnosable.
32
+ `--log-to` records provider discovery details and raw usage parsing diagnostics so empty or unexpected windows are easier to debug.
33
+
34
+ ## Providers
35
+
36
+ - Codex
37
+ - Claude
38
+ - Copilot
39
+ - Antigravity
40
+
41
+ ## Preview
25
42
 
43
+ <img width="2301" height="1397" alt="letmecode preview" src="https://github.com/user-attachments/assets/c37d6847-9926-4977-8592-5cab8346a86f" />
26
44
 
27
45
  ## Local development
28
46
 
29
47
  ```bash
30
48
  pnpm install
49
+ pnpm test
31
50
  pnpm start
32
51
  ```
@@ -4,6 +4,7 @@ export function parseCliOptions(argv) {
4
4
  let showHelp = false;
5
5
  let verbose = false;
6
6
  let logToPath;
7
+ let enableAnonymousUsageReporting = true;
7
8
  for (let index = 0; index < argv.length; index += 1) {
8
9
  const argument = argv[index] ?? "";
9
10
  if (argument === "-h" || argument === "--help") {
@@ -29,9 +30,13 @@ export function parseCliOptions(argv) {
29
30
  throw new Error("Expected a file path after --log-to=.");
30
31
  }
31
32
  logToPath = value;
33
+ continue;
34
+ }
35
+ if (argument === "--no-usage") {
36
+ enableAnonymousUsageReporting = false;
32
37
  }
33
38
  }
34
- return { showHelp, verbose, logToPath };
39
+ return { showHelp, verbose, logToPath, enableAnonymousUsageReporting };
35
40
  }
36
41
  export function buildProviderStatsOptions(options) {
37
42
  return {
@@ -41,7 +46,7 @@ export function buildProviderStatsOptions(options) {
41
46
  }
42
47
  export function buildHelpText() {
43
48
  return [
44
- "letmecode - provider-based terminal usage dashboard",
49
+ "letmecode - terminal AI usage dashboard",
45
50
  "",
46
51
  "Usage:",
47
52
  " letmecode [options]",
@@ -50,6 +55,7 @@ export function buildHelpText() {
50
55
  " -h, --help Show this help and exit",
51
56
  " -v, --verbose Show extra provider warnings",
52
57
  " --log-to PATH Write trace logs to PATH",
58
+ " --no-usage Disable anonymous usage reporting",
53
59
  "",
54
60
  "Controls:",
55
61
  " [ ] / Tab Switch providers",
@@ -64,7 +70,11 @@ export function buildHelpText() {
64
70
  " --log-to PATH writes Claude detection details,",
65
71
  " session root selection, parsed session file summaries, aggregated usage selection,",
66
72
  " every candidate binary path check, the final found/not-found result,",
67
- " and the raw /usage command output plus live window matching details."
73
+ " and the raw /usage command output plus live window matching details.",
74
+ "",
75
+ "Anonymous reporting:",
76
+ " Enabled by default. Use --no-usage to disable the best-effort",
77
+ " anonymous usage summary upload."
68
78
  ].join("\n");
69
79
  }
70
80
  export function createFileTraceLogger(logPath) {
@@ -4,6 +4,7 @@ import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, rende
4
4
  import { buildHelpText, buildProviderStatsOptions, parseCliOptions } from "./cli-options.js";
5
5
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
6
6
  import { reportAnonymousUsage } from "./reporting.js";
7
+ import { estimateLimitFullValue } from "./providers/limits.js";
7
8
  const ESC = String.fromCharCode(0x1b);
8
9
  // Normal mouse tracking (button press/release only) + SGR extended coordinates.
9
10
  // This makes the tabs clickable while leaving Shift+drag native text selection
@@ -19,7 +20,7 @@ const DETAIL_TABS = [
19
20
  { id: "usage-by-model", label: "Models" }
20
21
  ];
21
22
  const CODEX_CREDIT_COST_USD = 0.01;
22
- const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq."];
23
+ const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq.", "Full"];
23
24
  const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
24
25
  const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
25
26
  const COPILOT_ACTIONS = [
@@ -96,7 +97,9 @@ function App(props) {
96
97
  setSelectedProviderId(topProvider.provider.id);
97
98
  }, [hasUserSelectedProvider, providerStates, sortedProviderStates]);
98
99
  useEffect(() => {
99
- if (hasReportedAnonymousUsageRef.current || providerStates.some((state) => state.status === "loading")) {
100
+ if (!props.usageReportingEnabled ||
101
+ hasReportedAnonymousUsageRef.current ||
102
+ providerStates.some((state) => state.status === "loading")) {
100
103
  return;
101
104
  }
102
105
  hasReportedAnonymousUsageRef.current = true;
@@ -106,7 +109,7 @@ function App(props) {
106
109
  void reportAnonymousUsage(readyStats).catch(() => {
107
110
  // Anonymous usage reporting is best-effort and must never disturb the TUI.
108
111
  });
109
- }, [providerStates]);
112
+ }, [props.usageReportingEnabled, providerStates]);
110
113
  useMouseClick((click) => {
111
114
  const regionId = resolveClick(click);
112
115
  if (!regionId) {
@@ -415,7 +418,10 @@ function buildLimitWindowTableRow(window) {
415
418
  formatCompactLocalDateTime(window.endTimeUtcIso),
416
419
  // Status-aware: shows "-" when the API-equivalent cost is unknown rather
417
420
  // than a misleading $0.00.
418
- formatUsageUsd(window.totals)
421
+ formatUsageUsd(window.totals),
422
+ // Extrapolated full value of the limit, rounded to a single figure here;
423
+ // the details panel shows the unrounded ±1% range.
424
+ formatLimitFullValueCompact(window.totals, window.maxUsedPercent)
419
425
  ]
420
426
  };
421
427
  }
@@ -459,7 +465,7 @@ function SelectionDetailsPanel(props) {
459
465
  }
460
466
  if (props.tabId === "limit-windows" && props.selectedLimitRow) {
461
467
  const row = props.selectedLimitRow;
462
- return (_jsx(DetailsPanelFrame, { children: _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }) }));
468
+ return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals), note: "API equivalent cost" }), _jsx(DetailRow, { label: "Full value", value: formatLimitFullValueRange(row.totals, row.maxUsedPercent), note: "API equivalent cost if 100% will be used" })] }));
463
469
  }
464
470
  if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
465
471
  const row = props.selectedDayRow;
@@ -477,7 +483,7 @@ function DetailRow(props) {
477
483
  const labelText = props.noSlice
478
484
  ? props.label.padEnd(props.padLength ?? 14)
479
485
  : pad(props.label, props.padLength ?? 14);
480
- return (_jsxs(Text, { children: [labelText, props.value] }));
486
+ return (_jsxs(Text, { children: [labelText, props.value, props.note ? _jsx(Text, { color: "gray", children: ` ${props.note}` }) : null] }));
481
487
  }
482
488
  function UsageTotalsDetails(props) {
483
489
  const { totals } = props;
@@ -563,6 +569,37 @@ function formatUsd(value) {
563
569
  maximumFractionDigits: 2
564
570
  });
565
571
  }
572
+ function formatUsdWhole(value) {
573
+ return Math.round(value).toLocaleString("en-US", {
574
+ currency: "USD",
575
+ style: "currency",
576
+ minimumFractionDigits: 0,
577
+ maximumFractionDigits: 0
578
+ });
579
+ }
580
+ // Extrapolate the limit's full USD value from the observed API-equivalent cost
581
+ // and how much of the limit it represents. Uses the highest reported percent
582
+ // (the latest cumulative usage) and returns "-" when the cost is unknown or the
583
+ // percent is missing.
584
+ function limitFullValueUsd(totals, usedPercent) {
585
+ if (totals.estimatedCreditsStatus === "unavailable") {
586
+ return null;
587
+ }
588
+ const usedUsd = totals.estimatedCredits * CODEX_CREDIT_COST_USD;
589
+ return estimateLimitFullValue(usedUsd, usedPercent);
590
+ }
591
+ function formatLimitFullValueCompact(totals, usedPercent) {
592
+ const estimate = limitFullValueUsd(totals, usedPercent);
593
+ return estimate ? formatUsdWhole(estimate.point) : "-";
594
+ }
595
+ function formatLimitFullValueRange(totals, usedPercent) {
596
+ const estimate = limitFullValueUsd(totals, usedPercent);
597
+ if (!estimate) {
598
+ return "-";
599
+ }
600
+ const low = formatUsd(estimate.low);
601
+ return Number.isFinite(estimate.high) ? `${low} – ${formatUsd(estimate.high)}` : `≥ ${low}`;
602
+ }
566
603
  function formatUnitUsd(value) {
567
604
  if (!Number.isFinite(value)) {
568
605
  return "-";
@@ -1002,7 +1039,7 @@ export function main(argv = process.argv.slice(2)) {
1002
1039
  restoreFullscreen();
1003
1040
  };
1004
1041
  process.once("exit", exitHandler);
1005
- const instance = render(_jsx(App, { statsOptions: statsOptions }), {
1042
+ const instance = render(_jsx(App, { statsOptions: statsOptions, usageReportingEnabled: cliOptions.enableAnonymousUsageReporting }), {
1006
1043
  stdout: process.stdout,
1007
1044
  stdin: process.stdin,
1008
1045
  stderr: process.stderr
@@ -236,10 +236,20 @@ function clampPercent(value) {
236
236
  function deduplicateRecords(records) {
237
237
  const byKey = new Map();
238
238
  for (const record of records) {
239
- byKey.set(`${record.sessionId}:${record.responseId}`, record);
239
+ const key = `${record.sessionId}:${record.responseId}`;
240
+ const existing = byKey.get(key);
241
+ // The RPC may surface the same response more than once (e.g. progressive
242
+ // snapshots in unspecified order). Keep the largest coherent total rather
243
+ // than trusting iteration order, so we never undercount a final snapshot.
244
+ if (!existing || recordTokenTotal(record) > recordTokenTotal(existing)) {
245
+ byKey.set(key, record);
246
+ }
240
247
  }
241
248
  return [...byKey.values()];
242
249
  }
250
+ function recordTokenTotal(record) {
251
+ return record.input + record.cacheRead + record.cacheWrite + record.output;
252
+ }
243
253
  function usageRecordToTotals(modelId, record) {
244
254
  return {
245
255
  inputTokens: record.input,
@@ -255,11 +265,12 @@ function usageRecordToTotals(modelId, record) {
255
265
  record.output,
256
266
  estimatedCredits: creditsFor(modelId, record),
257
267
  eventCount: 1,
258
- // The local RPC reports cache reads but never cache writes, so cache reads
259
- // are accurate while cache writes are genuinely unknown (not a confirmed
260
- // zero) surfaced as "-" everywhere, including the input/output ratio.
268
+ // The local RPC reports cache reads but never cache writes, so a zero cache
269
+ // write is genuinely unknown (not a confirmed zero) and is surfaced as "-".
270
+ // A positive value only appears when a source explicitly reports it, in
271
+ // which case it is both billed (see creditsFor) and shown as known.
261
272
  cacheReadStatus: "known",
262
- cacheWriteStatus: "unavailable",
273
+ cacheWriteStatus: record.cacheWrite > 0 ? "known" : "unavailable",
263
274
  estimatedCreditsStatus: rateForModel(modelId, record.input)
264
275
  ? "known"
265
276
  : "unavailable"
@@ -240,7 +240,9 @@ function creditsFor(modelId, usage, timestampMs) {
240
240
  return 0;
241
241
  }
242
242
  const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
243
- const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
243
+ // The US inference surcharge must match regardless of the casing the source
244
+ // reports (e.g. "us", "US"), so compare case-insensitively.
245
+ const inferenceMultiplier = usage.inferenceGeo.trim().toLowerCase() === "us" ? 1.1 : 1;
244
246
  return (((usage.inputTokens / 1000000) * rate.input +
245
247
  (usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
246
248
  (cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
@@ -566,15 +568,20 @@ function mergeParsedUsageEvents(previous, next) {
566
568
  rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
567
569
  };
568
570
  }
569
- // Pick the snapshot that carries the most usage. Cumulative snapshots are monotonic, so the
570
- // largest total is the final state; this also keeps a real synthetic-followup row (0 tokens)
571
- // from clobbering the real usage it follows. Ties fall back to the later, then the earlier-seen
572
- // event for deterministic output.
571
+ // Pick the snapshot with the latest timestamp. Same-key events are repeated/streamed snapshots
572
+ // of one logical request, so the most recent one reflects the final state. The one exception is
573
+ // a zero-usage internal <synthetic> completion marker, which is a real followup row rather than
574
+ // an updated snapshot and must not clobber the real usage it follows.
573
575
  function selectMergedSnapshotEvent(previous, next) {
574
- if (next.totals.totalTokens !== previous.totals.totalTokens) {
575
- return next.totals.totalTokens > previous.totals.totalTokens ? next : previous;
576
+ const previousIsHollowSynthetic = isInternalClaudeModel(previous.modelId) && previous.totals.totalTokens === 0;
577
+ const nextIsHollowSynthetic = isInternalClaudeModel(next.modelId) && next.totals.totalTokens === 0;
578
+ if (nextIsHollowSynthetic && !previousIsHollowSynthetic) {
579
+ return previous;
576
580
  }
577
- return normalizeTimestamp(next.timestampMs) > normalizeTimestamp(previous.timestampMs) ? next : previous;
581
+ if (previousIsHollowSynthetic && !nextIsHollowSynthetic) {
582
+ return next;
583
+ }
584
+ return normalizeTimestamp(next.timestampMs) >= normalizeTimestamp(previous.timestampMs) ? next : previous;
578
585
  }
579
586
  function selectMergedEventModelId(primary, other) {
580
587
  if (primary.modelId === other.modelId) {
@@ -7,10 +7,16 @@ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTota
7
7
  import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
8
8
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
9
9
  import { resolveUsageRate } from "./pricing.js";
10
+ // One credit equals $0.01 (see CODEX_CREDIT_COST_USD in index.tsx), so credits
11
+ // equal USD * 100. Rate cards are expressed in the model's actual API price in
12
+ // USD per 1M tokens and scaled to credits in creditsFor, matching the Claude
13
+ // provider. These are the real gpt-5.* API prices, not the (4x cheaper) Codex
14
+ // subscription credit prices.
15
+ const USD_TO_CREDITS = 100;
10
16
  const RATE_CARD = {
11
- "gpt-5.5": { input: 125, cacheRead: 12.5, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 125, output: 750 },
12
- "gpt-5.4": { input: 62.5, cacheRead: 6.25, cacheWrite: 62.5, cacheWrite5m: 62.5, cacheWrite1h: 62.5, output: 375 },
13
- "gpt-5.4-mini": { input: 18.75, cacheRead: 1.875, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 18.75, output: 113 }
17
+ "gpt-5.5": { input: 5, cacheRead: 0.5, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 30 },
18
+ "gpt-5.4": { input: 2.5, cacheRead: 0.25, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 15 },
19
+ "gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0.75, cacheWrite5m: 0.75, cacheWrite1h: 0.75, output: 4.5 }
14
20
  };
15
21
  export class CodexUsageProvider extends UsageProviderBase {
16
22
  constructor(options = {}) {
@@ -235,9 +241,10 @@ function creditsFor(modelId, usage) {
235
241
  }
236
242
  const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
237
243
  const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
238
- return ((nonCachedInputTokens / 1000000) * rate.input +
244
+ return (((nonCachedInputTokens / 1000000) * rate.input +
239
245
  (cachedInputTokens / 1000000) * rate.cacheRead +
240
- (usage.outputTokens / 1000000) * rate.output);
246
+ (usage.outputTokens / 1000000) * rate.output) *
247
+ USD_TO_CREDITS);
241
248
  }
242
249
  function rawUsageToTotals(usage) {
243
250
  const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
@@ -3,11 +3,42 @@ export function createLimitWindowAggregates() {
3
3
  return new Map();
4
4
  }
5
5
  export function numberOrZero(value) {
6
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
6
+ if (typeof value === "number") {
7
+ return Number.isFinite(value) ? value : 0;
8
+ }
9
+ if (typeof value === "string") {
10
+ const trimmed = value.trim();
11
+ if (trimmed === "") {
12
+ return 0;
13
+ }
14
+ const parsed = Number(trimmed);
15
+ return Number.isFinite(parsed) ? parsed : 0;
16
+ }
17
+ return 0;
7
18
  }
8
19
  export function asRecord(value) {
9
20
  return value && typeof value === "object" ? value : null;
10
21
  }
22
+ /**
23
+ * Extrapolate the full value of a limit from a partial observation: if
24
+ * `usedValue` represents `usedPercent` of the limit, the full limit is
25
+ * `usedValue / (usedPercent / 100)`. Because `usedPercent` is only known
26
+ * approximately, a `±percentTolerance` band yields a low/high range around the
27
+ * point estimate. Returns null when there is nothing to extrapolate from
28
+ * (no observed value, or a non-positive percent).
29
+ */
30
+ export function estimateLimitFullValue(usedValue, usedPercent, percentTolerance = 1) {
31
+ if (!(usedValue > 0) || !(usedPercent > 0)) {
32
+ return null;
33
+ }
34
+ const toFull = (percent) => usedValue / (percent / 100);
35
+ const upperPercent = usedPercent - percentTolerance;
36
+ return {
37
+ point: toFull(usedPercent),
38
+ low: toFull(usedPercent + percentTolerance),
39
+ high: upperPercent > 0 ? toFull(upperPercent) : Infinity
40
+ };
41
+ }
11
42
  export function applyRateLimits(windows, rateLimits, eventTimeMs, modelId, deltaTotals, planTypes) {
12
43
  if (!rateLimits) {
13
44
  return;
@@ -4,7 +4,9 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  const REPORTING_ENDPOINT = "https://devforth.io/admin/api/report_ussage_anonymous";
6
6
  const CREDIT_TO_DOLLARS = 0.01;
7
- const MIN_REPORTED_USED_PERCENTS = 1;
7
+ // Limit windows at or below this used-percent carry too little signal to be
8
+ // worth reporting, so they are dropped from the anonymous usage payload.
9
+ const SKIP_REPORT_USED_PERCENTS = 3;
8
10
  let versionCache = null;
9
11
  export async function reportAnonymousUsage(statsList) {
10
12
  const payload = await buildAnonymousUsagePayload(statsList);
@@ -103,7 +105,7 @@ function resolveReportedUsedPercents(window) {
103
105
  return clampPercent(window.maxUsedPercent - window.minUsedPercent);
104
106
  }
105
107
  function shouldReportUsageWindow(window) {
106
- return resolveReportedUsedPercents(window) >= MIN_REPORTED_USED_PERCENTS;
108
+ return resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS;
107
109
  }
108
110
  function clampPercent(value) {
109
111
  if (!Number.isFinite(value)) {
package/package.json CHANGED
@@ -1,13 +1,22 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.22",
4
- "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
- "author": "devforth.io",
3
+ "version": "0.1.24",
4
+ "description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
5
+ "author": "Devforth (https://devforth.io)",
6
6
  "license": "MIT",
7
7
  "type": "commonjs",
8
+ "main": "./dist/index.js",
8
9
  "bin": {
9
10
  "letmecode": "./bin/letmecode.js"
10
11
  },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/devforth/letmecode.git"
15
+ },
16
+ "homepage": "https://github.com/devforth/letmecode#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/devforth/letmecode/issues"
19
+ },
11
20
  "files": [
12
21
  "bin",
13
22
  "dist",
@@ -21,9 +30,18 @@
21
30
  "access": "public"
22
31
  },
23
32
  "keywords": [
33
+ "ai",
34
+ "agents",
24
35
  "cli",
25
- "ink",
36
+ "dashboard",
37
+ "usage",
38
+ "terminal",
39
+ "codex",
40
+ "claude",
41
+ "copilot",
42
+ "antigravity",
26
43
  "npx",
44
+ "ink",
27
45
  "typescript"
28
46
  ],
29
47
  "dependencies": {