letmecode 0.1.6 → 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 +106 -37
- package/package.json +11 -13
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
|
|
@@ -61,7 +61,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
61
61
|
const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root);
|
|
62
62
|
const sessionsRoot = resolvedSessionsRoot.rootPath;
|
|
63
63
|
const agentName = normalizeAnalyticsAgentName(this.label);
|
|
64
|
-
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);
|
|
65
65
|
const byModel = new Map();
|
|
66
66
|
const byDay = createDailyUsageAggregates();
|
|
67
67
|
const windows = createLimitWindowAggregates();
|
|
@@ -131,6 +131,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
131
131
|
usageCommandKind: this.usageCommandKind,
|
|
132
132
|
readUsageCommandOutput: this.readUsageCommandOutput,
|
|
133
133
|
readAuthStatusOutput: this.readAuthStatusOutput,
|
|
134
|
+
traceLogger: options.traceLogger,
|
|
134
135
|
now: this.now(),
|
|
135
136
|
selectedEvents
|
|
136
137
|
});
|
|
@@ -470,12 +471,35 @@ function normalizeTimestamp(value) {
|
|
|
470
471
|
function extractRateLimits(payloadObject, message) {
|
|
471
472
|
return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
|
|
472
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
|
+
}
|
|
473
496
|
async function buildLiveLimitWindows(options) {
|
|
474
497
|
const [usageOutput, subscriptionType] = await Promise.all([
|
|
475
|
-
readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput),
|
|
476
|
-
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)
|
|
477
500
|
]);
|
|
478
501
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
502
|
+
traceClaude(options.traceLogger, options.usageCommandKind, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
479
503
|
const resolvedPlanType = subscriptionType || "live";
|
|
480
504
|
return {
|
|
481
505
|
primaryLimitWindows: snapshots
|
|
@@ -486,30 +510,38 @@ async function buildLiveLimitWindows(options) {
|
|
|
486
510
|
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now))
|
|
487
511
|
};
|
|
488
512
|
}
|
|
489
|
-
async function readClaudeSubscriptionType(root, usageCommandKind, override) {
|
|
490
|
-
const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
|
|
491
|
-
|
|
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;
|
|
492
518
|
}
|
|
493
|
-
async function readClaudeAuthStatusOutput(root, usageCommandKind, override) {
|
|
519
|
+
async function readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger) {
|
|
494
520
|
if (override) {
|
|
495
521
|
try {
|
|
496
|
-
|
|
522
|
+
const output = await override();
|
|
523
|
+
traceClaude(traceLogger, usageCommandKind, "Using injected auth status output override.");
|
|
524
|
+
return output;
|
|
497
525
|
}
|
|
498
526
|
catch {
|
|
527
|
+
traceClaude(traceLogger, usageCommandKind, "Injected auth status output override failed.");
|
|
499
528
|
return null;
|
|
500
529
|
}
|
|
501
530
|
}
|
|
502
531
|
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
503
532
|
const cached = claudeAuthStatusOutputCache.get(cacheKey);
|
|
504
533
|
if (cached) {
|
|
534
|
+
traceClaude(traceLogger, usageCommandKind, "Auth status output cache hit.");
|
|
505
535
|
return cached;
|
|
506
536
|
}
|
|
507
537
|
const pending = (async () => {
|
|
508
|
-
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
|
|
538
|
+
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
|
|
509
539
|
if (!binaryPath) {
|
|
540
|
+
traceClaude(traceLogger, usageCommandKind, "Skipping auth status command because no Claude binary was found.");
|
|
510
541
|
return null;
|
|
511
542
|
}
|
|
512
543
|
try {
|
|
544
|
+
traceClaude(traceLogger, usageCommandKind, `Running auth status command with ${binaryPath}.`);
|
|
513
545
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
|
|
514
546
|
encoding: "utf8",
|
|
515
547
|
maxBuffer: 1024 * 1024,
|
|
@@ -517,10 +549,12 @@ async function readClaudeAuthStatusOutput(root, usageCommandKind, override) {
|
|
|
517
549
|
windowsHide: true
|
|
518
550
|
});
|
|
519
551
|
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
552
|
+
traceClaude(traceLogger, usageCommandKind, "Auth status command completed successfully.");
|
|
520
553
|
return combined || null;
|
|
521
554
|
}
|
|
522
555
|
catch (error) {
|
|
523
556
|
const combined = extractExecOutput(error);
|
|
557
|
+
traceClaude(traceLogger, usageCommandKind, `Auth status command failed: ${formatErrorMessage(error)}.`);
|
|
524
558
|
return combined || null;
|
|
525
559
|
}
|
|
526
560
|
})();
|
|
@@ -557,34 +591,42 @@ function parseClaudeAuthStatusSnapshot(output) {
|
|
|
557
591
|
return null;
|
|
558
592
|
}
|
|
559
593
|
}
|
|
560
|
-
async function readClaudeUserIdHash(root, usageCommandKind, override, agentName) {
|
|
561
|
-
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);
|
|
562
596
|
const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
|
|
563
597
|
if (!snapshot) {
|
|
564
598
|
return null;
|
|
565
599
|
}
|
|
566
600
|
return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
|
|
567
601
|
}
|
|
568
|
-
async function readClaudeUsageCommandOutput(root, usageCommandKind, override) {
|
|
602
|
+
async function readClaudeUsageCommandOutput(root, usageCommandKind, override, traceLogger) {
|
|
569
603
|
if (override) {
|
|
570
604
|
try {
|
|
571
|
-
|
|
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;
|
|
572
609
|
}
|
|
573
610
|
catch {
|
|
611
|
+
traceClaude(traceLogger, usageCommandKind, "Injected /usage output override failed.");
|
|
574
612
|
return null;
|
|
575
613
|
}
|
|
576
614
|
}
|
|
577
615
|
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
578
616
|
const cached = claudeUsageOutputCache.get(cacheKey);
|
|
579
617
|
if (cached) {
|
|
618
|
+
traceClaude(traceLogger, usageCommandKind, "Usage output cache hit.");
|
|
580
619
|
return cached;
|
|
581
620
|
}
|
|
582
621
|
const pending = (async () => {
|
|
583
|
-
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
|
|
622
|
+
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
|
|
584
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>");
|
|
585
626
|
return null;
|
|
586
627
|
}
|
|
587
628
|
try {
|
|
629
|
+
traceClaude(traceLogger, usageCommandKind, `Running /usage command with ${binaryPath}.`);
|
|
588
630
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
|
|
589
631
|
encoding: "utf8",
|
|
590
632
|
maxBuffer: 1024 * 1024,
|
|
@@ -592,10 +634,14 @@ async function readClaudeUsageCommandOutput(root, usageCommandKind, override) {
|
|
|
592
634
|
windowsHide: true
|
|
593
635
|
});
|
|
594
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)}`);
|
|
595
639
|
return combined || null;
|
|
596
640
|
}
|
|
597
641
|
catch (error) {
|
|
598
642
|
const combined = extractExecOutput(error);
|
|
643
|
+
traceClaude(traceLogger, usageCommandKind, `Usage command failed: ${formatErrorMessage(error)}.`);
|
|
644
|
+
traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(combined || null)}`);
|
|
599
645
|
return combined || null;
|
|
600
646
|
}
|
|
601
647
|
})();
|
|
@@ -610,51 +656,68 @@ function extractExecOutput(error) {
|
|
|
610
656
|
const stderr = typeof error.stderr === "string" ? error.stderr : "";
|
|
611
657
|
return [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
612
658
|
}
|
|
613
|
-
async function resolveClaudeBinaryPath(root, usageCommandKind) {
|
|
659
|
+
async function resolveClaudeBinaryPath(root, usageCommandKind, traceLogger) {
|
|
614
660
|
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
615
661
|
const cached = claudeBinaryPathCache.get(cacheKey);
|
|
616
662
|
if (cached) {
|
|
617
|
-
|
|
663
|
+
const binaryPath = await cached;
|
|
664
|
+
traceClaude(traceLogger, usageCommandKind, `Binary detection cache hit: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
|
|
665
|
+
return binaryPath;
|
|
618
666
|
}
|
|
619
|
-
const pending =
|
|
620
|
-
|
|
621
|
-
|
|
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
|
+
})();
|
|
622
675
|
claudeBinaryPathCache.set(cacheKey, pending);
|
|
623
676
|
return pending;
|
|
624
677
|
}
|
|
625
|
-
async function resolveVsCodeClaudeBinaryPath(root) {
|
|
678
|
+
async function resolveVsCodeClaudeBinaryPath(root, traceLogger) {
|
|
626
679
|
const boosterDirectories = [
|
|
627
680
|
path.join(root, ".vscode", "extensions"),
|
|
628
681
|
path.join(root, ".vscode-server", "extensions"),
|
|
629
682
|
path.join(root, ".vscode-server-insiders", "extensions")
|
|
630
683
|
];
|
|
684
|
+
let firstFoundPath = null;
|
|
631
685
|
for (const directory of boosterDirectories) {
|
|
632
|
-
const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory);
|
|
633
|
-
if (binaryPath) {
|
|
634
|
-
|
|
686
|
+
const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger);
|
|
687
|
+
if (!firstFoundPath && binaryPath) {
|
|
688
|
+
firstFoundPath = binaryPath;
|
|
635
689
|
}
|
|
636
690
|
}
|
|
637
|
-
return
|
|
691
|
+
return firstFoundPath;
|
|
638
692
|
}
|
|
639
|
-
async function resolveClaudeBinaryFromExtensionDirectory(directory) {
|
|
693
|
+
async function resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger) {
|
|
640
694
|
let entries;
|
|
695
|
+
traceClaude(traceLogger, "vscode", `Scanning extension directory ${directory}.`);
|
|
641
696
|
try {
|
|
642
697
|
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
643
698
|
}
|
|
644
|
-
catch {
|
|
699
|
+
catch (error) {
|
|
700
|
+
traceClaude(traceLogger, "vscode", `Could not read ${directory}: ${formatErrorMessage(error)}.`);
|
|
645
701
|
return null;
|
|
646
702
|
}
|
|
647
703
|
const candidates = entries
|
|
648
704
|
.filter((entry) => entry.isDirectory() && entry.name.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX))
|
|
649
705
|
.map((entry) => entry.name)
|
|
650
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;
|
|
651
712
|
for (const candidate of candidates) {
|
|
652
713
|
const binaryPath = path.join(directory, candidate, "resources", "native-binary", "claude");
|
|
653
|
-
|
|
654
|
-
|
|
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;
|
|
655
718
|
}
|
|
656
719
|
}
|
|
657
|
-
return
|
|
720
|
+
return firstFoundPath;
|
|
658
721
|
}
|
|
659
722
|
function compareClaudeExtensionDirectoryNames(left, right) {
|
|
660
723
|
const leftVersion = extractClaudeExtensionVersion(left);
|
|
@@ -678,25 +741,31 @@ function extractClaudeExtensionVersion(directoryName) {
|
|
|
678
741
|
.map((part) => Number(part))
|
|
679
742
|
.filter((part) => Number.isFinite(part));
|
|
680
743
|
}
|
|
681
|
-
async function resolveCliClaudeBinaryPath(root) {
|
|
744
|
+
async function resolveCliClaudeBinaryPath(root, traceLogger) {
|
|
682
745
|
const directCandidates = [
|
|
683
746
|
path.join(root, ".local", "bin", "claude"),
|
|
684
747
|
path.join(root, "bin", "claude")
|
|
685
748
|
];
|
|
749
|
+
let firstFoundPath = null;
|
|
686
750
|
for (const candidate of directCandidates) {
|
|
687
|
-
|
|
688
|
-
|
|
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;
|
|
689
755
|
}
|
|
690
756
|
}
|
|
691
|
-
return
|
|
757
|
+
return firstFoundPath;
|
|
692
758
|
}
|
|
693
|
-
async function
|
|
759
|
+
async function checkReadableExecutableFile(filePath) {
|
|
694
760
|
try {
|
|
695
761
|
await fs.promises.access(filePath, fs.constants.R_OK | fs.constants.X_OK);
|
|
696
|
-
return true;
|
|
762
|
+
return { ok: true };
|
|
697
763
|
}
|
|
698
|
-
catch {
|
|
699
|
-
return
|
|
764
|
+
catch (error) {
|
|
765
|
+
return {
|
|
766
|
+
ok: false,
|
|
767
|
+
errorMessage: formatErrorMessage(error)
|
|
768
|
+
};
|
|
700
769
|
}
|
|
701
770
|
}
|
|
702
771
|
function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Provider-based terminal usage dashboard for LetMeCode.",
|
|
5
5
|
"author": "devforth.io",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"packageManager": "pnpm@10.28.2",
|
|
8
7
|
"type": "commonjs",
|
|
9
8
|
"bin": {
|
|
10
9
|
"letmecode": "./bin/letmecode.js"
|
|
@@ -21,16 +20,6 @@
|
|
|
21
20
|
"publishConfig": {
|
|
22
21
|
"access": "public"
|
|
23
22
|
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
26
|
-
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
27
|
-
"prepack": "npm run build",
|
|
28
|
-
"prestart": "npm run build",
|
|
29
|
-
"start": "node ./bin/letmecode.js",
|
|
30
|
-
"pretest": "npm run build",
|
|
31
|
-
"smoke": "node ./bin/letmecode.js",
|
|
32
|
-
"test": "node --test ink-app/test/*.test.mjs"
|
|
33
|
-
},
|
|
34
23
|
"keywords": [
|
|
35
24
|
"cli",
|
|
36
25
|
"ink",
|
|
@@ -47,5 +36,14 @@
|
|
|
47
36
|
"@types/node": "^24.0.7",
|
|
48
37
|
"@types/react": "^18.3.24",
|
|
49
38
|
"typescript": "^5.8.3"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
42
|
+
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
43
|
+
"prestart": "npm run build",
|
|
44
|
+
"start": "node ./bin/letmecode.js",
|
|
45
|
+
"pretest": "npm run build",
|
|
46
|
+
"smoke": "node ./bin/letmecode.js",
|
|
47
|
+
"test": "node --test ink-app/test/*.test.mjs"
|
|
50
48
|
}
|
|
51
|
-
}
|
|
49
|
+
}
|