letmecode 0.1.21 → 0.1.23
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 +21 -0
- package/README.md +29 -10
- package/ink-app/dist/cli-options.js +13 -3
- package/ink-app/dist/index.js +5 -3
- package/ink-app/dist/providers/claude.js +111 -37
- package/ink-app/dist/providers/pricing.js +5 -0
- package/ink-app/dist/reporting.js +5 -1
- package/package.json +22 -4
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
|
-
#
|
|
1
|
+
# LetMeCode
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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`
|
|
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 -
|
|
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) {
|
package/ink-app/dist/index.js
CHANGED
|
@@ -96,7 +96,9 @@ function App(props) {
|
|
|
96
96
|
setSelectedProviderId(topProvider.provider.id);
|
|
97
97
|
}, [hasUserSelectedProvider, providerStates, sortedProviderStates]);
|
|
98
98
|
useEffect(() => {
|
|
99
|
-
if (
|
|
99
|
+
if (!props.usageReportingEnabled ||
|
|
100
|
+
hasReportedAnonymousUsageRef.current ||
|
|
101
|
+
providerStates.some((state) => state.status === "loading")) {
|
|
100
102
|
return;
|
|
101
103
|
}
|
|
102
104
|
hasReportedAnonymousUsageRef.current = true;
|
|
@@ -106,7 +108,7 @@ function App(props) {
|
|
|
106
108
|
void reportAnonymousUsage(readyStats).catch(() => {
|
|
107
109
|
// Anonymous usage reporting is best-effort and must never disturb the TUI.
|
|
108
110
|
});
|
|
109
|
-
}, [providerStates]);
|
|
111
|
+
}, [props.usageReportingEnabled, providerStates]);
|
|
110
112
|
useMouseClick((click) => {
|
|
111
113
|
const regionId = resolveClick(click);
|
|
112
114
|
if (!regionId) {
|
|
@@ -1002,7 +1004,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
1002
1004
|
restoreFullscreen();
|
|
1003
1005
|
};
|
|
1004
1006
|
process.once("exit", exitHandler);
|
|
1005
|
-
const instance = render(_jsx(App, { statsOptions: statsOptions }), {
|
|
1007
|
+
const instance = render(_jsx(App, { statsOptions: statsOptions, usageReportingEnabled: cliOptions.enableAnonymousUsageReporting }), {
|
|
1006
1008
|
stdout: process.stdout,
|
|
1007
1009
|
stdin: process.stdin,
|
|
1008
1010
|
stderr: process.stderr
|
|
@@ -10,12 +10,26 @@ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregate
|
|
|
10
10
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
11
11
|
import { resolveUsageRate } from "./pricing.js";
|
|
12
12
|
const RATE_CARD = {
|
|
13
|
+
"claude-fable-5": { input: 10, cacheRead: 1, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 20, output: 50 },
|
|
13
14
|
"claude-opus-4-8": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
14
15
|
"claude-opus-4-7": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
15
16
|
"claude-opus-4-6": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
16
17
|
"claude-opus-4-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
17
18
|
"claude-opus-4-1": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
18
19
|
"claude-opus-4": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
20
|
+
"claude-sonnet-5": {
|
|
21
|
+
input: 3,
|
|
22
|
+
cacheRead: 0.3,
|
|
23
|
+
cacheWrite: 3.75,
|
|
24
|
+
cacheWrite5m: 3.75,
|
|
25
|
+
cacheWrite1h: 6,
|
|
26
|
+
output: 15,
|
|
27
|
+
// Intro pricing (~0.66x) through 2026-08-31; reverts to the standard rate above on 2026-09-01.
|
|
28
|
+
introOffer: {
|
|
29
|
+
effectiveUntilMs: Date.UTC(2026, 8, 1),
|
|
30
|
+
rate: { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
19
33
|
"claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
20
34
|
"claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
21
35
|
"claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
@@ -46,12 +60,14 @@ const parsedClaudeSessionFilesCache = new Map();
|
|
|
46
60
|
const claudeBinaryPathCache = new Map();
|
|
47
61
|
const claudeUsageOutputCache = new Map();
|
|
48
62
|
const claudeAuthStatusOutputCache = new Map();
|
|
63
|
+
const claudeOauthCredentialsOutputCache = new Map();
|
|
49
64
|
export class ClaudeUsageProvider extends UsageProviderBase {
|
|
50
65
|
constructor(options = {}) {
|
|
51
66
|
super(options.id ?? "claude", options.label ?? "Claude");
|
|
52
67
|
this.root = path.resolve(options.root ?? os.homedir());
|
|
53
68
|
this.readUsageCommandOutput = options.readUsageCommandOutput;
|
|
54
69
|
this.readAuthStatusOutput = options.readAuthStatusOutput;
|
|
70
|
+
this.readOauthCredentials = options.readOauthCredentials;
|
|
55
71
|
this.now = options.now ?? (() => new Date());
|
|
56
72
|
}
|
|
57
73
|
async getStats(options = {}) {
|
|
@@ -147,7 +163,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
147
163
|
const liveLimitWindows = await buildLiveLimitWindows({
|
|
148
164
|
root: this.root,
|
|
149
165
|
readUsageCommandOutput: this.readUsageCommandOutput,
|
|
150
|
-
|
|
166
|
+
readOauthCredentials: this.readOauthCredentials,
|
|
151
167
|
traceLogger: options.traceLogger,
|
|
152
168
|
now: this.now(),
|
|
153
169
|
selectedEvents
|
|
@@ -212,14 +228,14 @@ function normalizeUsage(value) {
|
|
|
212
228
|
inferenceGeo: String(usage.inference_geo ?? "")
|
|
213
229
|
};
|
|
214
230
|
}
|
|
215
|
-
function resolveRate(modelId) {
|
|
216
|
-
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true });
|
|
231
|
+
function resolveRate(modelId, timestampMs) {
|
|
232
|
+
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true, timestampMs });
|
|
217
233
|
}
|
|
218
234
|
function isInternalClaudeModel(modelId) {
|
|
219
235
|
return modelId === "<synthetic>";
|
|
220
236
|
}
|
|
221
|
-
function creditsFor(modelId, usage) {
|
|
222
|
-
const rate = resolveRate(modelId);
|
|
237
|
+
function creditsFor(modelId, usage, timestampMs) {
|
|
238
|
+
const rate = resolveRate(modelId, timestampMs);
|
|
223
239
|
if (!rate) {
|
|
224
240
|
return 0;
|
|
225
241
|
}
|
|
@@ -233,7 +249,7 @@ function creditsFor(modelId, usage) {
|
|
|
233
249
|
inferenceMultiplier *
|
|
234
250
|
USD_TO_CREDITS);
|
|
235
251
|
}
|
|
236
|
-
function usageToTotals(modelId, usage) {
|
|
252
|
+
function usageToTotals(modelId, usage, timestampMs) {
|
|
237
253
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
238
254
|
const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
239
255
|
cacheWriteBreakdown.cacheWrite1hInputTokens;
|
|
@@ -249,7 +265,7 @@ function usageToTotals(modelId, usage) {
|
|
|
249
265
|
usage.cacheReadInputTokens +
|
|
250
266
|
cacheWriteInputTokens +
|
|
251
267
|
usage.outputTokens,
|
|
252
|
-
estimatedCredits: creditsFor(modelId, usage),
|
|
268
|
+
estimatedCredits: creditsFor(modelId, usage, timestampMs),
|
|
253
269
|
eventCount: 1
|
|
254
270
|
};
|
|
255
271
|
}
|
|
@@ -437,7 +453,7 @@ async function parseSessionFile(filePath) {
|
|
|
437
453
|
timestampMs: eventTimeMs,
|
|
438
454
|
modelId,
|
|
439
455
|
usage: normalizedUsage,
|
|
440
|
-
totals: usageToTotals(modelId, normalizedUsage),
|
|
456
|
+
totals: usageToTotals(modelId, normalizedUsage, eventTimeMs),
|
|
441
457
|
rateLimits
|
|
442
458
|
});
|
|
443
459
|
}
|
|
@@ -546,7 +562,7 @@ function mergeParsedUsageEvents(previous, next) {
|
|
|
546
562
|
timestampMs: Math.max(normalizeTimestamp(previous.timestampMs), normalizeTimestamp(next.timestampMs)),
|
|
547
563
|
modelId,
|
|
548
564
|
usage,
|
|
549
|
-
totals: usageToTotals(modelId, usage),
|
|
565
|
+
totals: usageToTotals(modelId, usage, primaryEvent.timestampMs),
|
|
550
566
|
rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
|
|
551
567
|
};
|
|
552
568
|
}
|
|
@@ -648,16 +664,16 @@ function buildClaudeCommandEnvironment() {
|
|
|
648
664
|
};
|
|
649
665
|
}
|
|
650
666
|
async function buildLiveLimitWindows(options) {
|
|
651
|
-
const [usageOutput,
|
|
667
|
+
const [usageOutput, credentials] = await Promise.all([
|
|
652
668
|
readClaudeUsageCommandOutput(options.root, options.readUsageCommandOutput, options.traceLogger),
|
|
653
|
-
|
|
669
|
+
readClaudeOauthCredentials(options.root, options.readOauthCredentials, options.traceLogger)
|
|
654
670
|
]);
|
|
655
671
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
656
672
|
traceClaude(options.traceLogger, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
657
673
|
if (snapshots.length === 0) {
|
|
658
674
|
traceClaude(options.traceLogger, "No live usage snapshots matched the expected /usage format.");
|
|
659
675
|
}
|
|
660
|
-
const resolvedPlanType = resolveClaudeLivePlanType(
|
|
676
|
+
const resolvedPlanType = resolveClaudeLivePlanType(credentials);
|
|
661
677
|
traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
|
|
662
678
|
const primaryLimitWindows = snapshots
|
|
663
679
|
.filter((snapshot) => snapshot.scope === "primary")
|
|
@@ -690,20 +706,82 @@ async function buildLiveLimitWindows(options) {
|
|
|
690
706
|
secondaryLimitWindows
|
|
691
707
|
};
|
|
692
708
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
709
|
+
// Reported as-is: the claude.ai OAuth `rateLimitTier` is not mapped to a friendly tier name
|
|
710
|
+
// anymore, it is combined with `subscriptionType` verbatim (e.g. "team|default_claude_max_5x").
|
|
711
|
+
function resolveClaudeLivePlanType(credentials) {
|
|
712
|
+
const subscriptionType = credentials?.subscriptionType;
|
|
713
|
+
if (!subscriptionType) {
|
|
714
|
+
return "live";
|
|
715
|
+
}
|
|
716
|
+
return credentials?.rateLimitTier ? `${subscriptionType}|${credentials.rateLimitTier}` : subscriptionType;
|
|
717
|
+
}
|
|
718
|
+
async function readClaudeOauthCredentials(root, override, traceLogger) {
|
|
719
|
+
const output = await readClaudeOauthCredentialsOutput(root, override, traceLogger);
|
|
720
|
+
const credentials = parseClaudeOauthCredentials(output);
|
|
721
|
+
if (output && !credentials) {
|
|
722
|
+
traceClaude(traceLogger, "Could not parse claude.ai OAuth credentials from credentials file.");
|
|
723
|
+
}
|
|
724
|
+
traceClaude(traceLogger, `OAuth credentials result: subscriptionType=${credentials?.subscriptionType ?? "<none>"} rateLimitTier=${credentials?.rateLimitTier ?? "<none>"}.`);
|
|
725
|
+
return credentials;
|
|
726
|
+
}
|
|
727
|
+
async function readClaudeOauthCredentialsOutput(root, override, traceLogger) {
|
|
728
|
+
if (override) {
|
|
729
|
+
try {
|
|
730
|
+
const output = await override();
|
|
731
|
+
traceClaude(traceLogger, "Using injected OAuth credentials override.");
|
|
732
|
+
return output;
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
traceClaude(traceLogger, "Injected OAuth credentials override failed.");
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const credentialsPath = path.join(path.resolve(root), ".claude", ".credentials.json");
|
|
740
|
+
const cacheKey = credentialsPath;
|
|
741
|
+
const cached = claudeOauthCredentialsOutputCache.get(cacheKey);
|
|
742
|
+
if (cached) {
|
|
743
|
+
traceClaude(traceLogger, "OAuth credentials cache hit.");
|
|
744
|
+
return cached;
|
|
696
745
|
}
|
|
697
|
-
|
|
746
|
+
const pending = (async () => {
|
|
747
|
+
try {
|
|
748
|
+
const output = await fs.promises.readFile(credentialsPath, { encoding: "utf8" });
|
|
749
|
+
traceClaude(traceLogger, `Read OAuth credentials from ${credentialsPath}.`);
|
|
750
|
+
return output;
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
traceClaude(traceLogger, `No readable OAuth credentials at ${credentialsPath}.`);
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
})();
|
|
757
|
+
claudeOauthCredentialsOutputCache.set(cacheKey, pending);
|
|
758
|
+
return pending;
|
|
698
759
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
760
|
+
function parseClaudeOauthCredentials(output) {
|
|
761
|
+
if (!output) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
let payload;
|
|
765
|
+
try {
|
|
766
|
+
payload = JSON.parse(output);
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const oauth = asRecord(asRecord(payload)?.claudeAiOauth);
|
|
772
|
+
if (!oauth) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const subscriptionType = typeof oauth.subscriptionType === "string" && oauth.subscriptionType.trim()
|
|
776
|
+
? oauth.subscriptionType.trim()
|
|
777
|
+
: null;
|
|
778
|
+
const rateLimitTier = typeof oauth.rateLimitTier === "string" && oauth.rateLimitTier.trim()
|
|
779
|
+
? oauth.rateLimitTier.trim()
|
|
780
|
+
: null;
|
|
781
|
+
if (!subscriptionType && !rateLimitTier) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
return { subscriptionType, rateLimitTier };
|
|
707
785
|
}
|
|
708
786
|
async function readClaudeAuthStatusOutput(root, override, traceLogger) {
|
|
709
787
|
if (override) {
|
|
@@ -751,10 +829,6 @@ async function readClaudeAuthStatusOutput(root, override, traceLogger) {
|
|
|
751
829
|
claudeAuthStatusOutputCache.set(cacheKey, pending);
|
|
752
830
|
return pending;
|
|
753
831
|
}
|
|
754
|
-
function parseClaudeSubscriptionType(output) {
|
|
755
|
-
const snapshot = parseClaudeAuthStatusSnapshot(output);
|
|
756
|
-
return snapshot?.subscriptionType ?? null;
|
|
757
|
-
}
|
|
758
832
|
function parseClaudeAuthStatusSnapshot(output) {
|
|
759
833
|
if (!output) {
|
|
760
834
|
return null;
|
|
@@ -977,9 +1051,6 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
977
1051
|
resetString: match[4]?.trim() || null
|
|
978
1052
|
});
|
|
979
1053
|
}
|
|
980
|
-
// Per-scope reset times printed by Claude. Lines like "Current week (Sonnet
|
|
981
|
-
// only)" omit the reset because it is identical to the "all models" week, so
|
|
982
|
-
// a missing reset inherits the resolved reset of the same scope.
|
|
983
1054
|
const resetMsByLabel = new Map();
|
|
984
1055
|
for (const parsed of parsedLines) {
|
|
985
1056
|
if (!parsed.resetString || resetMsByLabel.has(parsed.label)) {
|
|
@@ -996,14 +1067,17 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
996
1067
|
if (!resetsAtMs) {
|
|
997
1068
|
continue;
|
|
998
1069
|
}
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1070
|
+
const isModelScopedWeek = parsed.label === "week" && parsed.windowQualifier !== "" && parsed.windowQualifier !== "all models";
|
|
1071
|
+
const modelFamily = isModelScopedWeek ? parsed.windowQualifier.replace(/\s+only$/, "") : null;
|
|
1072
|
+
const limitId = modelFamily
|
|
1073
|
+
? `current-week-${modelFamily.replace(/\s+/g, "-")}-only`
|
|
1074
|
+
: `current-${parsed.label}`;
|
|
1001
1075
|
snapshots.set(limitId, {
|
|
1002
1076
|
scope: parsed.label === "session" ? "primary" : "secondary",
|
|
1003
1077
|
label: parsed.label,
|
|
1004
1078
|
limitId,
|
|
1005
|
-
|
|
1006
|
-
modelType:
|
|
1079
|
+
modelFamily,
|
|
1080
|
+
modelType: modelFamily ? parsed.windowQualifier : undefined,
|
|
1007
1081
|
usedPercent: parsed.usedPercent,
|
|
1008
1082
|
resetsAtMs,
|
|
1009
1083
|
windowMinutes: parsed.windowMinutes
|
|
@@ -1087,10 +1161,10 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1087
1161
|
};
|
|
1088
1162
|
}
|
|
1089
1163
|
function matchesClaudeLiveSnapshotModelScope(snapshot, modelId) {
|
|
1090
|
-
if (snapshot.
|
|
1164
|
+
if (!snapshot.modelFamily) {
|
|
1091
1165
|
return true;
|
|
1092
1166
|
}
|
|
1093
|
-
return modelId.toLowerCase().includes(
|
|
1167
|
+
return modelId.toLowerCase().includes(snapshot.modelFamily);
|
|
1094
1168
|
}
|
|
1095
1169
|
function buildModelUsageRowsForEvents(events) {
|
|
1096
1170
|
const byModel = new Map();
|
|
@@ -14,5 +14,10 @@ export function resolveUsageRate(rateCard, modelId, inputTokens = 0, options = {
|
|
|
14
14
|
if (rate.longContext && inputTokens > rate.longContext.thresholdTokens) {
|
|
15
15
|
return rate.longContext.rate;
|
|
16
16
|
}
|
|
17
|
+
if (rate.introOffer &&
|
|
18
|
+
Number.isFinite(options.timestampMs) &&
|
|
19
|
+
options.timestampMs < rate.introOffer.effectiveUntilMs) {
|
|
20
|
+
return rate.introOffer.rate;
|
|
21
|
+
}
|
|
17
22
|
return rate;
|
|
18
23
|
}
|
|
@@ -64,7 +64,11 @@ function resolveReportModelType(stats, window) {
|
|
|
64
64
|
return truncateSchemaString(stats.providerId, 128);
|
|
65
65
|
}
|
|
66
66
|
function resolveClaudeReportModelType(window) {
|
|
67
|
-
|
|
67
|
+
const modelFamily = window.modelType?.toLowerCase().replace(/\s+only$/, "").trim();
|
|
68
|
+
if (!modelFamily) {
|
|
69
|
+
return "all";
|
|
70
|
+
}
|
|
71
|
+
return truncateSchemaString(`${modelFamily.replace(/\s+/g, "-")}-only`, 128);
|
|
68
72
|
}
|
|
69
73
|
function resolveAntigravityReportModelType(stats, window) {
|
|
70
74
|
const limitId = window.limitId.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "devforth.io",
|
|
3
|
+
"version": "0.1.23",
|
|
4
|
+
"description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
|
|
5
|
+
"author": "Devforth (https://devforth.io)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"packageManager": "pnpm@10.28.2",
|
|
8
8
|
"type": "commonjs",
|
|
9
|
+
"main": "./dist/index.js",
|
|
9
10
|
"bin": {
|
|
10
11
|
"letmecode": "./bin/letmecode.js"
|
|
11
12
|
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/devforth/letmecode.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/devforth/letmecode#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/devforth/letmecode/issues"
|
|
20
|
+
},
|
|
12
21
|
"files": [
|
|
13
22
|
"bin",
|
|
14
23
|
"dist",
|
|
@@ -32,9 +41,18 @@
|
|
|
32
41
|
"test": "node --test ink-app/test/*.test.mjs"
|
|
33
42
|
},
|
|
34
43
|
"keywords": [
|
|
44
|
+
"ai",
|
|
45
|
+
"agents",
|
|
35
46
|
"cli",
|
|
36
|
-
"
|
|
47
|
+
"dashboard",
|
|
48
|
+
"usage",
|
|
49
|
+
"terminal",
|
|
50
|
+
"codex",
|
|
51
|
+
"claude",
|
|
52
|
+
"copilot",
|
|
53
|
+
"antigravity",
|
|
37
54
|
"npx",
|
|
55
|
+
"ink",
|
|
38
56
|
"typescript"
|
|
39
57
|
],
|
|
40
58
|
"dependencies": {
|