letmecode 0.1.23 → 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/ink-app/dist/index.js +39 -4
- package/ink-app/dist/providers/antigravity/provider.js +16 -5
- package/ink-app/dist/providers/claude.js +15 -8
- package/ink-app/dist/providers/codex.js +12 -5
- package/ink-app/dist/providers/limits.js +32 -1
- package/ink-app/dist/reporting.js +4 -2
- package/package.json +11 -13
package/ink-app/dist/index.js
CHANGED
|
@@ -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 = [
|
|
@@ -417,7 +418,10 @@ function buildLimitWindowTableRow(window) {
|
|
|
417
418
|
formatCompactLocalDateTime(window.endTimeUtcIso),
|
|
418
419
|
// Status-aware: shows "-" when the API-equivalent cost is unknown rather
|
|
419
420
|
// than a misleading $0.00.
|
|
420
|
-
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)
|
|
421
425
|
]
|
|
422
426
|
};
|
|
423
427
|
}
|
|
@@ -461,7 +465,7 @@ function SelectionDetailsPanel(props) {
|
|
|
461
465
|
}
|
|
462
466
|
if (props.tabId === "limit-windows" && props.selectedLimitRow) {
|
|
463
467
|
const row = props.selectedLimitRow;
|
|
464
|
-
return (
|
|
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" })] }));
|
|
465
469
|
}
|
|
466
470
|
if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
|
|
467
471
|
const row = props.selectedDayRow;
|
|
@@ -479,7 +483,7 @@ function DetailRow(props) {
|
|
|
479
483
|
const labelText = props.noSlice
|
|
480
484
|
? props.label.padEnd(props.padLength ?? 14)
|
|
481
485
|
: pad(props.label, props.padLength ?? 14);
|
|
482
|
-
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] }));
|
|
483
487
|
}
|
|
484
488
|
function UsageTotalsDetails(props) {
|
|
485
489
|
const { totals } = props;
|
|
@@ -565,6 +569,37 @@ function formatUsd(value) {
|
|
|
565
569
|
maximumFractionDigits: 2
|
|
566
570
|
});
|
|
567
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
|
+
}
|
|
568
603
|
function formatUnitUsd(value) {
|
|
569
604
|
if (!Number.isFinite(value)) {
|
|
570
605
|
return "-";
|
|
@@ -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
|
-
|
|
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
|
|
259
|
-
//
|
|
260
|
-
//
|
|
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
|
-
|
|
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
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
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
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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:
|
|
12
|
-
"gpt-5.4": { input:
|
|
13
|
-
"gpt-5.4-mini": { input:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
|
|
5
5
|
"author": "Devforth (https://devforth.io)",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"packageManager": "pnpm@10.28.2",
|
|
8
7
|
"type": "commonjs",
|
|
9
8
|
"main": "./dist/index.js",
|
|
10
9
|
"bin": {
|
|
@@ -30,16 +29,6 @@
|
|
|
30
29
|
"publishConfig": {
|
|
31
30
|
"access": "public"
|
|
32
31
|
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
35
|
-
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
36
|
-
"prepack": "npm run build",
|
|
37
|
-
"prestart": "npm run build",
|
|
38
|
-
"start": "node ./bin/letmecode.js",
|
|
39
|
-
"pretest": "npm run build",
|
|
40
|
-
"smoke": "node ./bin/letmecode.js",
|
|
41
|
-
"test": "node --test ink-app/test/*.test.mjs"
|
|
42
|
-
},
|
|
43
32
|
"keywords": [
|
|
44
33
|
"ai",
|
|
45
34
|
"agents",
|
|
@@ -64,5 +53,14 @@
|
|
|
64
53
|
"@types/node": "^24.0.7",
|
|
65
54
|
"@types/react": "^18.3.24",
|
|
66
55
|
"typescript": "^5.8.3"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
59
|
+
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
60
|
+
"prestart": "npm run build",
|
|
61
|
+
"start": "node ./bin/letmecode.js",
|
|
62
|
+
"pretest": "npm run build",
|
|
63
|
+
"smoke": "node ./bin/letmecode.js",
|
|
64
|
+
"test": "node --test ink-app/test/*.test.mjs"
|
|
67
65
|
}
|
|
68
|
-
}
|
|
66
|
+
}
|