letmecode 0.1.5 → 0.1.7
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/README.md +2 -0
- package/ink-app/dist/cli-options.js +89 -0
- package/ink-app/dist/index.js +8 -6
- package/ink-app/dist/providers/claude.js +192 -41
- package/ink-app/dist/reporting.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function parseCliOptions(argv) {
|
|
4
|
+
let showHelp = false;
|
|
5
|
+
let verbose = false;
|
|
6
|
+
let logToPath;
|
|
7
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
8
|
+
const argument = argv[index] ?? "";
|
|
9
|
+
if (argument === "-h" || argument === "--help") {
|
|
10
|
+
showHelp = true;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (argument === "-v" || argument === "--verbose") {
|
|
14
|
+
verbose = true;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (argument === "--log-to") {
|
|
18
|
+
const nextArgument = argv[index + 1];
|
|
19
|
+
if (!nextArgument) {
|
|
20
|
+
throw new Error("Expected a file path after --log-to.");
|
|
21
|
+
}
|
|
22
|
+
logToPath = nextArgument;
|
|
23
|
+
index += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (argument.startsWith("--log-to=")) {
|
|
27
|
+
const value = argument.slice("--log-to=".length);
|
|
28
|
+
if (!value) {
|
|
29
|
+
throw new Error("Expected a file path after --log-to=.");
|
|
30
|
+
}
|
|
31
|
+
logToPath = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { showHelp, verbose, logToPath };
|
|
35
|
+
}
|
|
36
|
+
export function buildProviderStatsOptions(options) {
|
|
37
|
+
return {
|
|
38
|
+
verbose: options.verbose,
|
|
39
|
+
traceLogger: options.logToPath ? createFileTraceLogger(options.logToPath) : undefined
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function buildHelpText() {
|
|
43
|
+
return [
|
|
44
|
+
"letmecode - provider-based terminal usage dashboard",
|
|
45
|
+
"",
|
|
46
|
+
"Usage:",
|
|
47
|
+
" letmecode [options]",
|
|
48
|
+
"",
|
|
49
|
+
"Options:",
|
|
50
|
+
" -h, --help Show this help and exit",
|
|
51
|
+
" -v, --verbose Show extra provider warnings",
|
|
52
|
+
" --log-to PATH Write trace logs to PATH",
|
|
53
|
+
"",
|
|
54
|
+
"Controls:",
|
|
55
|
+
" [ ] / Tab Switch providers",
|
|
56
|
+
" Shift+Tab Switch providers backward",
|
|
57
|
+
" j / k Switch dashboard sections",
|
|
58
|
+
" Up / Down Switch dashboard sections",
|
|
59
|
+
" Left / Right Select the previous or next row",
|
|
60
|
+
" 1, h / l, Enter Run Copilot setup actions",
|
|
61
|
+
" q or Esc Quit",
|
|
62
|
+
"",
|
|
63
|
+
"Trace logging:",
|
|
64
|
+
" --log-to PATH writes Claude CLI SDK and Claude VSCode detection details,",
|
|
65
|
+
" every candidate binary path check, the final found/not-found result,",
|
|
66
|
+
" and the raw /usage command output."
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
export function createFileTraceLogger(logPath) {
|
|
70
|
+
const resolvedPath = path.resolve(logPath);
|
|
71
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(resolvedPath, [
|
|
73
|
+
"# letmecode trace",
|
|
74
|
+
`# started_at=${new Date().toISOString()}`,
|
|
75
|
+
`# cwd=${process.cwd()}`,
|
|
76
|
+
`# argv=${JSON.stringify(process.argv.slice(2))}`,
|
|
77
|
+
""
|
|
78
|
+
].join("\n"), "utf8");
|
|
79
|
+
return {
|
|
80
|
+
log(message) {
|
|
81
|
+
const timestamp = new Date().toISOString();
|
|
82
|
+
const formatted = message
|
|
83
|
+
.split(/\r?\n/)
|
|
84
|
+
.map((line, index) => (index === 0 ? `[${timestamp}] ${line}` : ` ${line}`))
|
|
85
|
+
.join("\n");
|
|
86
|
+
fs.appendFileSync(resolvedPath, `${formatted}\n`, "utf8");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
package/ink-app/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, render } from "ink";
|
|
4
|
+
import { buildHelpText, buildProviderStatsOptions, parseCliOptions } from "./cli-options.js";
|
|
4
5
|
import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
|
|
5
6
|
import { reportAnonymousUsage } from "./reporting.js";
|
|
6
7
|
const ESC = String.fromCharCode(0x1b);
|
|
@@ -769,11 +770,6 @@ function getDayRows(providerState) {
|
|
|
769
770
|
function getLimitRowKey(row) {
|
|
770
771
|
return `${row.scope}-${row.planType}-${row.limitId}-${row.startTimeUtcIso}-${row.endTimeUtcIso}`;
|
|
771
772
|
}
|
|
772
|
-
function parseStatsOptions(argv) {
|
|
773
|
-
return {
|
|
774
|
-
verbose: argv.includes("-v") || argv.includes("--verbose")
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
773
|
function useViewportHeight() {
|
|
778
774
|
const { stdout } = useStdout();
|
|
779
775
|
const [viewportHeight, setViewportHeight] = useState(() => resolveViewportHeight(stdout.rows));
|
|
@@ -799,6 +795,12 @@ function resolveViewportHeight(rows) {
|
|
|
799
795
|
return Math.max(1, terminalRows - 1);
|
|
800
796
|
}
|
|
801
797
|
export function main(argv = process.argv.slice(2)) {
|
|
798
|
+
const cliOptions = parseCliOptions(argv);
|
|
799
|
+
if (cliOptions.showHelp) {
|
|
800
|
+
process.stdout.write(`${buildHelpText()}\n`);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const statsOptions = buildProviderStatsOptions(cliOptions);
|
|
802
804
|
const restoreFullscreen = enterFullscreenMode(process.stdout);
|
|
803
805
|
const disableMouse = enableMouseReporting(process.stdout);
|
|
804
806
|
const exitHandler = () => {
|
|
@@ -806,7 +808,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
806
808
|
restoreFullscreen();
|
|
807
809
|
};
|
|
808
810
|
process.once("exit", exitHandler);
|
|
809
|
-
const instance = render(_jsx(App, { statsOptions:
|
|
811
|
+
const instance = render(_jsx(App, { statsOptions: statsOptions }), {
|
|
810
812
|
stdout: process.stdout,
|
|
811
813
|
stdin: process.stdin,
|
|
812
814
|
stderr: process.stderr
|
|
@@ -58,9 +58,10 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
58
58
|
this.now = options.now ?? (() => new Date());
|
|
59
59
|
}
|
|
60
60
|
async getStats(options = {}) {
|
|
61
|
-
const
|
|
61
|
+
const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root);
|
|
62
|
+
const sessionsRoot = resolvedSessionsRoot.rootPath;
|
|
62
63
|
const agentName = normalizeAnalyticsAgentName(this.label);
|
|
63
|
-
const userIdHash = await readClaudeUserIdHash(this.root, this.usageCommandKind, this.readAuthStatusOutput, agentName);
|
|
64
|
+
const userIdHash = await readClaudeUserIdHash(this.root, this.usageCommandKind, this.readAuthStatusOutput, agentName, options.traceLogger);
|
|
64
65
|
const byModel = new Map();
|
|
65
66
|
const byDay = createDailyUsageAggregates();
|
|
66
67
|
const windows = createLimitWindowAggregates();
|
|
@@ -130,6 +131,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
130
131
|
usageCommandKind: this.usageCommandKind,
|
|
131
132
|
readUsageCommandOutput: this.readUsageCommandOutput,
|
|
132
133
|
readAuthStatusOutput: this.readAuthStatusOutput,
|
|
134
|
+
traceLogger: options.traceLogger,
|
|
133
135
|
now: this.now(),
|
|
134
136
|
selectedEvents
|
|
135
137
|
});
|
|
@@ -149,7 +151,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
149
151
|
totals: summaryTotals,
|
|
150
152
|
distinctModels: modelUsage.map((row) => row.modelId),
|
|
151
153
|
distinctPlanTypes: [...planTypes].sort(),
|
|
152
|
-
rootLabel:
|
|
154
|
+
rootLabel: resolvedSessionsRoot.rootLabel,
|
|
153
155
|
rootPath: sessionsRoot
|
|
154
156
|
},
|
|
155
157
|
modelUsage,
|
|
@@ -236,7 +238,87 @@ function resolveClaudeCacheWriteBreakdown(usage) {
|
|
|
236
238
|
};
|
|
237
239
|
}
|
|
238
240
|
function isSessionFile(filePath) {
|
|
239
|
-
return filePath.endsWith(".jsonl")
|
|
241
|
+
return filePath.endsWith(".jsonl");
|
|
242
|
+
}
|
|
243
|
+
async function resolveClaudeSessionsRoot(root) {
|
|
244
|
+
const candidates = buildClaudeSessionsRootCandidates(root);
|
|
245
|
+
for (const candidate of candidates) {
|
|
246
|
+
if (await isDirectory(candidate.rootPath)) {
|
|
247
|
+
return candidate;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return candidates[0] ?? {
|
|
251
|
+
rootLabel: "~/.claude/projects",
|
|
252
|
+
rootPath: path.join(path.resolve(root), ".claude", "projects")
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function buildClaudeSessionsRootCandidates(root) {
|
|
256
|
+
const resolvedRoot = path.resolve(root);
|
|
257
|
+
const baseName = path.basename(resolvedRoot);
|
|
258
|
+
const parentBaseName = path.basename(path.dirname(resolvedRoot));
|
|
259
|
+
const candidates = [];
|
|
260
|
+
if (baseName === "projects") {
|
|
261
|
+
if (parentBaseName === ".claude") {
|
|
262
|
+
candidates.push({
|
|
263
|
+
rootLabel: "~/.claude/projects",
|
|
264
|
+
rootPath: resolvedRoot
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else if (parentBaseName === "claude" || parentBaseName === "Claude") {
|
|
268
|
+
candidates.push({
|
|
269
|
+
rootLabel: `~/.config/${parentBaseName}/projects`,
|
|
270
|
+
rootPath: resolvedRoot
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
candidates.push({
|
|
275
|
+
rootLabel: "projects",
|
|
276
|
+
rootPath: resolvedRoot
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (baseName === ".claude") {
|
|
281
|
+
candidates.push({
|
|
282
|
+
rootLabel: "~/.claude/projects",
|
|
283
|
+
rootPath: path.join(resolvedRoot, "projects")
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (parentBaseName === ".config" && (baseName === "claude" || baseName === "Claude")) {
|
|
287
|
+
candidates.push({
|
|
288
|
+
rootLabel: `~/.config/${baseName}/projects`,
|
|
289
|
+
rootPath: path.join(resolvedRoot, "projects")
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
candidates.push({
|
|
293
|
+
rootLabel: "~/.claude/projects",
|
|
294
|
+
rootPath: path.join(resolvedRoot, ".claude", "projects")
|
|
295
|
+
}, {
|
|
296
|
+
rootLabel: "~/.config/claude/projects",
|
|
297
|
+
rootPath: path.join(resolvedRoot, ".config", "claude", "projects")
|
|
298
|
+
}, {
|
|
299
|
+
rootLabel: "~/.config/Claude/projects",
|
|
300
|
+
rootPath: path.join(resolvedRoot, ".config", "Claude", "projects")
|
|
301
|
+
});
|
|
302
|
+
const dedupedCandidates = new Map();
|
|
303
|
+
for (const candidate of candidates) {
|
|
304
|
+
const normalizedPath = path.resolve(candidate.rootPath);
|
|
305
|
+
if (!dedupedCandidates.has(normalizedPath)) {
|
|
306
|
+
dedupedCandidates.set(normalizedPath, {
|
|
307
|
+
rootLabel: candidate.rootLabel,
|
|
308
|
+
rootPath: normalizedPath
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return [...dedupedCandidates.values()];
|
|
313
|
+
}
|
|
314
|
+
async function isDirectory(directoryPath) {
|
|
315
|
+
try {
|
|
316
|
+
const stats = await fs.promises.stat(directoryPath);
|
|
317
|
+
return stats.isDirectory();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
240
322
|
}
|
|
241
323
|
async function* walkSessionFiles(directory) {
|
|
242
324
|
let entries;
|
|
@@ -389,12 +471,35 @@ function normalizeTimestamp(value) {
|
|
|
389
471
|
function extractRateLimits(payloadObject, message) {
|
|
390
472
|
return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
|
|
391
473
|
}
|
|
474
|
+
function traceClaude(traceLogger, usageCommandKind, message) {
|
|
475
|
+
if (!traceLogger) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const targetLabel = usageCommandKind === "vscode" ? "Claude VSCode" : "Claude";
|
|
479
|
+
traceLogger.log(`[${targetLabel}] ${message}`);
|
|
480
|
+
}
|
|
481
|
+
function formatErrorMessage(error) {
|
|
482
|
+
if (!error) {
|
|
483
|
+
return "Unknown error";
|
|
484
|
+
}
|
|
485
|
+
if (error instanceof Error && error.message) {
|
|
486
|
+
return error.message;
|
|
487
|
+
}
|
|
488
|
+
return String(error);
|
|
489
|
+
}
|
|
490
|
+
function describeUsageOutput(output) {
|
|
491
|
+
if (output == null) {
|
|
492
|
+
return "<null>";
|
|
493
|
+
}
|
|
494
|
+
return output.trim() ? output : "<empty>";
|
|
495
|
+
}
|
|
392
496
|
async function buildLiveLimitWindows(options) {
|
|
393
497
|
const [usageOutput, subscriptionType] = await Promise.all([
|
|
394
|
-
readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput),
|
|
395
|
-
readClaudeSubscriptionType(options.root, options.usageCommandKind, options.readAuthStatusOutput)
|
|
498
|
+
readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput, options.traceLogger),
|
|
499
|
+
readClaudeSubscriptionType(options.root, options.usageCommandKind, options.readAuthStatusOutput, options.traceLogger)
|
|
396
500
|
]);
|
|
397
501
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
502
|
+
traceClaude(options.traceLogger, options.usageCommandKind, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
398
503
|
const resolvedPlanType = subscriptionType || "live";
|
|
399
504
|
return {
|
|
400
505
|
primaryLimitWindows: snapshots
|
|
@@ -405,30 +510,38 @@ async function buildLiveLimitWindows(options) {
|
|
|
405
510
|
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now))
|
|
406
511
|
};
|
|
407
512
|
}
|
|
408
|
-
async function readClaudeSubscriptionType(root, usageCommandKind, override) {
|
|
409
|
-
const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
|
|
410
|
-
|
|
513
|
+
async function readClaudeSubscriptionType(root, usageCommandKind, override, traceLogger) {
|
|
514
|
+
const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
|
|
515
|
+
const subscriptionType = parseClaudeSubscriptionType(output);
|
|
516
|
+
traceClaude(traceLogger, usageCommandKind, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
|
|
517
|
+
return subscriptionType;
|
|
411
518
|
}
|
|
412
|
-
async function readClaudeAuthStatusOutput(root, usageCommandKind, override) {
|
|
519
|
+
async function readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger) {
|
|
413
520
|
if (override) {
|
|
414
521
|
try {
|
|
415
|
-
|
|
522
|
+
const output = await override();
|
|
523
|
+
traceClaude(traceLogger, usageCommandKind, "Using injected auth status output override.");
|
|
524
|
+
return output;
|
|
416
525
|
}
|
|
417
526
|
catch {
|
|
527
|
+
traceClaude(traceLogger, usageCommandKind, "Injected auth status output override failed.");
|
|
418
528
|
return null;
|
|
419
529
|
}
|
|
420
530
|
}
|
|
421
531
|
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
422
532
|
const cached = claudeAuthStatusOutputCache.get(cacheKey);
|
|
423
533
|
if (cached) {
|
|
534
|
+
traceClaude(traceLogger, usageCommandKind, "Auth status output cache hit.");
|
|
424
535
|
return cached;
|
|
425
536
|
}
|
|
426
537
|
const pending = (async () => {
|
|
427
|
-
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
|
|
538
|
+
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
|
|
428
539
|
if (!binaryPath) {
|
|
540
|
+
traceClaude(traceLogger, usageCommandKind, "Skipping auth status command because no Claude binary was found.");
|
|
429
541
|
return null;
|
|
430
542
|
}
|
|
431
543
|
try {
|
|
544
|
+
traceClaude(traceLogger, usageCommandKind, `Running auth status command with ${binaryPath}.`);
|
|
432
545
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
|
|
433
546
|
encoding: "utf8",
|
|
434
547
|
maxBuffer: 1024 * 1024,
|
|
@@ -436,10 +549,12 @@ async function readClaudeAuthStatusOutput(root, usageCommandKind, override) {
|
|
|
436
549
|
windowsHide: true
|
|
437
550
|
});
|
|
438
551
|
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
552
|
+
traceClaude(traceLogger, usageCommandKind, "Auth status command completed successfully.");
|
|
439
553
|
return combined || null;
|
|
440
554
|
}
|
|
441
555
|
catch (error) {
|
|
442
556
|
const combined = extractExecOutput(error);
|
|
557
|
+
traceClaude(traceLogger, usageCommandKind, `Auth status command failed: ${formatErrorMessage(error)}.`);
|
|
443
558
|
return combined || null;
|
|
444
559
|
}
|
|
445
560
|
})();
|
|
@@ -476,34 +591,42 @@ function parseClaudeAuthStatusSnapshot(output) {
|
|
|
476
591
|
return null;
|
|
477
592
|
}
|
|
478
593
|
}
|
|
479
|
-
async function readClaudeUserIdHash(root, usageCommandKind, override, agentName) {
|
|
480
|
-
const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
|
|
594
|
+
async function readClaudeUserIdHash(root, usageCommandKind, override, agentName, traceLogger) {
|
|
595
|
+
const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
|
|
481
596
|
const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
|
|
482
597
|
if (!snapshot) {
|
|
483
598
|
return null;
|
|
484
599
|
}
|
|
485
600
|
return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
|
|
486
601
|
}
|
|
487
|
-
async function readClaudeUsageCommandOutput(root, usageCommandKind, override) {
|
|
602
|
+
async function readClaudeUsageCommandOutput(root, usageCommandKind, override, traceLogger) {
|
|
488
603
|
if (override) {
|
|
489
604
|
try {
|
|
490
|
-
|
|
605
|
+
const output = await override();
|
|
606
|
+
traceClaude(traceLogger, usageCommandKind, "Using injected /usage output override.");
|
|
607
|
+
traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(output)}`);
|
|
608
|
+
return output;
|
|
491
609
|
}
|
|
492
610
|
catch {
|
|
611
|
+
traceClaude(traceLogger, usageCommandKind, "Injected /usage output override failed.");
|
|
493
612
|
return null;
|
|
494
613
|
}
|
|
495
614
|
}
|
|
496
615
|
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
497
616
|
const cached = claudeUsageOutputCache.get(cacheKey);
|
|
498
617
|
if (cached) {
|
|
618
|
+
traceClaude(traceLogger, usageCommandKind, "Usage output cache hit.");
|
|
499
619
|
return cached;
|
|
500
620
|
}
|
|
501
621
|
const pending = (async () => {
|
|
502
|
-
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
|
|
622
|
+
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
|
|
503
623
|
if (!binaryPath) {
|
|
624
|
+
traceClaude(traceLogger, usageCommandKind, "Skipping /usage command because no Claude binary was found.");
|
|
625
|
+
traceClaude(traceLogger, usageCommandKind, "Usage returned:\n<not available>");
|
|
504
626
|
return null;
|
|
505
627
|
}
|
|
506
628
|
try {
|
|
629
|
+
traceClaude(traceLogger, usageCommandKind, `Running /usage command with ${binaryPath}.`);
|
|
507
630
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
|
|
508
631
|
encoding: "utf8",
|
|
509
632
|
maxBuffer: 1024 * 1024,
|
|
@@ -511,10 +634,14 @@ async function readClaudeUsageCommandOutput(root, usageCommandKind, override) {
|
|
|
511
634
|
windowsHide: true
|
|
512
635
|
});
|
|
513
636
|
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
637
|
+
traceClaude(traceLogger, usageCommandKind, "Usage command completed successfully.");
|
|
638
|
+
traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(combined || null)}`);
|
|
514
639
|
return combined || null;
|
|
515
640
|
}
|
|
516
641
|
catch (error) {
|
|
517
642
|
const combined = extractExecOutput(error);
|
|
643
|
+
traceClaude(traceLogger, usageCommandKind, `Usage command failed: ${formatErrorMessage(error)}.`);
|
|
644
|
+
traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(combined || null)}`);
|
|
518
645
|
return combined || null;
|
|
519
646
|
}
|
|
520
647
|
})();
|
|
@@ -529,50 +656,68 @@ function extractExecOutput(error) {
|
|
|
529
656
|
const stderr = typeof error.stderr === "string" ? error.stderr : "";
|
|
530
657
|
return [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
531
658
|
}
|
|
532
|
-
async function resolveClaudeBinaryPath(root, usageCommandKind) {
|
|
659
|
+
async function resolveClaudeBinaryPath(root, usageCommandKind, traceLogger) {
|
|
533
660
|
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
534
661
|
const cached = claudeBinaryPathCache.get(cacheKey);
|
|
535
662
|
if (cached) {
|
|
536
|
-
|
|
663
|
+
const binaryPath = await cached;
|
|
664
|
+
traceClaude(traceLogger, usageCommandKind, `Binary detection cache hit: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
|
|
665
|
+
return binaryPath;
|
|
537
666
|
}
|
|
538
|
-
const pending =
|
|
539
|
-
|
|
540
|
-
|
|
667
|
+
const pending = (async () => {
|
|
668
|
+
traceClaude(traceLogger, usageCommandKind, `Starting binary detection under ${root}.`);
|
|
669
|
+
const binaryPath = usageCommandKind === "vscode"
|
|
670
|
+
? await resolveVsCodeClaudeBinaryPath(root, traceLogger)
|
|
671
|
+
: await resolveCliClaudeBinaryPath(root, traceLogger);
|
|
672
|
+
traceClaude(traceLogger, usageCommandKind, `Binary detection result: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
|
|
673
|
+
return binaryPath;
|
|
674
|
+
})();
|
|
541
675
|
claudeBinaryPathCache.set(cacheKey, pending);
|
|
542
676
|
return pending;
|
|
543
677
|
}
|
|
544
|
-
async function resolveVsCodeClaudeBinaryPath(root) {
|
|
678
|
+
async function resolveVsCodeClaudeBinaryPath(root, traceLogger) {
|
|
545
679
|
const boosterDirectories = [
|
|
546
680
|
path.join(root, ".vscode", "extensions"),
|
|
547
|
-
path.join(root, ".vscode-server", "extensions")
|
|
681
|
+
path.join(root, ".vscode-server", "extensions"),
|
|
682
|
+
path.join(root, ".vscode-server-insiders", "extensions")
|
|
548
683
|
];
|
|
684
|
+
let firstFoundPath = null;
|
|
549
685
|
for (const directory of boosterDirectories) {
|
|
550
|
-
const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory);
|
|
551
|
-
if (binaryPath) {
|
|
552
|
-
|
|
686
|
+
const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger);
|
|
687
|
+
if (!firstFoundPath && binaryPath) {
|
|
688
|
+
firstFoundPath = binaryPath;
|
|
553
689
|
}
|
|
554
690
|
}
|
|
555
|
-
return
|
|
691
|
+
return firstFoundPath;
|
|
556
692
|
}
|
|
557
|
-
async function resolveClaudeBinaryFromExtensionDirectory(directory) {
|
|
693
|
+
async function resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger) {
|
|
558
694
|
let entries;
|
|
695
|
+
traceClaude(traceLogger, "vscode", `Scanning extension directory ${directory}.`);
|
|
559
696
|
try {
|
|
560
697
|
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
561
698
|
}
|
|
562
|
-
catch {
|
|
699
|
+
catch (error) {
|
|
700
|
+
traceClaude(traceLogger, "vscode", `Could not read ${directory}: ${formatErrorMessage(error)}.`);
|
|
563
701
|
return null;
|
|
564
702
|
}
|
|
565
703
|
const candidates = entries
|
|
566
704
|
.filter((entry) => entry.isDirectory() && entry.name.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX))
|
|
567
705
|
.map((entry) => entry.name)
|
|
568
706
|
.sort(compareClaudeExtensionDirectoryNames);
|
|
707
|
+
if (candidates.length === 0) {
|
|
708
|
+
traceClaude(traceLogger, "vscode", `No Claude VSCode extension candidates found in ${directory}.`);
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
let firstFoundPath = null;
|
|
569
712
|
for (const candidate of candidates) {
|
|
570
713
|
const binaryPath = path.join(directory, candidate, "resources", "native-binary", "claude");
|
|
571
|
-
|
|
572
|
-
|
|
714
|
+
const accessCheck = await checkReadableExecutableFile(binaryPath);
|
|
715
|
+
traceClaude(traceLogger, "vscode", `Checked ${binaryPath} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
|
|
716
|
+
if (!firstFoundPath && accessCheck.ok) {
|
|
717
|
+
firstFoundPath = binaryPath;
|
|
573
718
|
}
|
|
574
719
|
}
|
|
575
|
-
return
|
|
720
|
+
return firstFoundPath;
|
|
576
721
|
}
|
|
577
722
|
function compareClaudeExtensionDirectoryNames(left, right) {
|
|
578
723
|
const leftVersion = extractClaudeExtensionVersion(left);
|
|
@@ -596,25 +741,31 @@ function extractClaudeExtensionVersion(directoryName) {
|
|
|
596
741
|
.map((part) => Number(part))
|
|
597
742
|
.filter((part) => Number.isFinite(part));
|
|
598
743
|
}
|
|
599
|
-
async function resolveCliClaudeBinaryPath(root) {
|
|
744
|
+
async function resolveCliClaudeBinaryPath(root, traceLogger) {
|
|
600
745
|
const directCandidates = [
|
|
601
746
|
path.join(root, ".local", "bin", "claude"),
|
|
602
747
|
path.join(root, "bin", "claude")
|
|
603
748
|
];
|
|
749
|
+
let firstFoundPath = null;
|
|
604
750
|
for (const candidate of directCandidates) {
|
|
605
|
-
|
|
606
|
-
|
|
751
|
+
const accessCheck = await checkReadableExecutableFile(candidate);
|
|
752
|
+
traceClaude(traceLogger, "cli", `Checked ${candidate} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
|
|
753
|
+
if (!firstFoundPath && accessCheck.ok) {
|
|
754
|
+
firstFoundPath = candidate;
|
|
607
755
|
}
|
|
608
756
|
}
|
|
609
|
-
return
|
|
757
|
+
return firstFoundPath;
|
|
610
758
|
}
|
|
611
|
-
async function
|
|
759
|
+
async function checkReadableExecutableFile(filePath) {
|
|
612
760
|
try {
|
|
613
761
|
await fs.promises.access(filePath, fs.constants.R_OK | fs.constants.X_OK);
|
|
614
|
-
return true;
|
|
762
|
+
return { ok: true };
|
|
615
763
|
}
|
|
616
|
-
catch {
|
|
617
|
-
return
|
|
764
|
+
catch (error) {
|
|
765
|
+
return {
|
|
766
|
+
ok: false,
|
|
767
|
+
errorMessage: formatErrorMessage(error)
|
|
768
|
+
};
|
|
618
769
|
}
|
|
619
770
|
}
|
|
620
771
|
function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
@@ -37,9 +37,25 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
|
|
|
37
37
|
used_percents: resolveReportedUsedPercents(window),
|
|
38
38
|
used_exhausted: window.maxUsedPercent >= 100,
|
|
39
39
|
value_dollars: roundDollars(window.totals.estimatedCredits * CREDIT_TO_DOLLARS),
|
|
40
|
+
usage_raw: buildUsageRaw(stats.providerId, window),
|
|
40
41
|
letmecode_version: letmecodeVersion
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
function buildUsageRaw(providerId, window) {
|
|
45
|
+
const usageRaw = {
|
|
46
|
+
output: window.totals.outputTokens,
|
|
47
|
+
input_non_cache: window.totals.inputTokens,
|
|
48
|
+
input_cache_read: window.totals.cacheReadInputTokens
|
|
49
|
+
};
|
|
50
|
+
if (isAnthropicProvider(providerId)) {
|
|
51
|
+
usageRaw.input_cache_w5m = window.totals.cacheWrite5mInputTokens;
|
|
52
|
+
usageRaw.input_cache_w1h = window.totals.cacheWrite1hInputTokens;
|
|
53
|
+
}
|
|
54
|
+
return usageRaw;
|
|
55
|
+
}
|
|
56
|
+
function isAnthropicProvider(providerId) {
|
|
57
|
+
return providerId === "claude" || providerId === "claude-vscode";
|
|
58
|
+
}
|
|
43
59
|
function resolveReportedUsedPercents(window) {
|
|
44
60
|
if (window.minUsedPercent === window.maxUsedPercent) {
|
|
45
61
|
return clampPercent(window.maxUsedPercent);
|