letmecode 0.1.6 → 0.1.8

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 CHANGED
@@ -4,6 +4,8 @@ Ussage:
4
4
 
5
5
  ```bash
6
6
  npx -y letmecode
7
+ npx -y letmecode -- -h
8
+ npx -y letmecode -- --log-to log.txt
7
9
  ```
8
10
 
9
11
  <img width="2308" height="1491" alt="image" src="https://github.com/user-attachments/assets/f3f52d79-00e3-4ff5-bf2f-65f8be632aaa" />
@@ -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
+ }
@@ -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: parseStatsOptions(argv) }), {
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,41 @@ 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
+ }
496
+ function buildClaudeCommandEnvironment() {
497
+ return {
498
+ ...process.env,
499
+ TZ: "UTC"
500
+ };
501
+ }
473
502
  async function buildLiveLimitWindows(options) {
474
503
  const [usageOutput, subscriptionType] = await Promise.all([
475
- readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput),
476
- readClaudeSubscriptionType(options.root, options.usageCommandKind, options.readAuthStatusOutput)
504
+ readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput, options.traceLogger),
505
+ readClaudeSubscriptionType(options.root, options.usageCommandKind, options.readAuthStatusOutput, options.traceLogger)
477
506
  ]);
478
507
  const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
508
+ traceClaude(options.traceLogger, options.usageCommandKind, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
479
509
  const resolvedPlanType = subscriptionType || "live";
480
510
  return {
481
511
  primaryLimitWindows: snapshots
@@ -486,41 +516,52 @@ async function buildLiveLimitWindows(options) {
486
516
  .map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now))
487
517
  };
488
518
  }
489
- async function readClaudeSubscriptionType(root, usageCommandKind, override) {
490
- const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
491
- return parseClaudeSubscriptionType(output);
519
+ async function readClaudeSubscriptionType(root, usageCommandKind, override, traceLogger) {
520
+ const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
521
+ const subscriptionType = parseClaudeSubscriptionType(output);
522
+ traceClaude(traceLogger, usageCommandKind, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
523
+ return subscriptionType;
492
524
  }
493
- async function readClaudeAuthStatusOutput(root, usageCommandKind, override) {
525
+ async function readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger) {
494
526
  if (override) {
495
527
  try {
496
- return await override();
528
+ const output = await override();
529
+ traceClaude(traceLogger, usageCommandKind, "Using injected auth status output override.");
530
+ return output;
497
531
  }
498
532
  catch {
533
+ traceClaude(traceLogger, usageCommandKind, "Injected auth status output override failed.");
499
534
  return null;
500
535
  }
501
536
  }
502
537
  const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
503
538
  const cached = claudeAuthStatusOutputCache.get(cacheKey);
504
539
  if (cached) {
540
+ traceClaude(traceLogger, usageCommandKind, "Auth status output cache hit.");
505
541
  return cached;
506
542
  }
507
543
  const pending = (async () => {
508
- const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
544
+ const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
509
545
  if (!binaryPath) {
546
+ traceClaude(traceLogger, usageCommandKind, "Skipping auth status command because no Claude binary was found.");
510
547
  return null;
511
548
  }
512
549
  try {
550
+ traceClaude(traceLogger, usageCommandKind, `Running auth status command with ${binaryPath} (TZ=UTC).`);
513
551
  const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
514
552
  encoding: "utf8",
553
+ env: buildClaudeCommandEnvironment(),
515
554
  maxBuffer: 1024 * 1024,
516
555
  timeout: 15000,
517
556
  windowsHide: true
518
557
  });
519
558
  const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
559
+ traceClaude(traceLogger, usageCommandKind, "Auth status command completed successfully.");
520
560
  return combined || null;
521
561
  }
522
562
  catch (error) {
523
563
  const combined = extractExecOutput(error);
564
+ traceClaude(traceLogger, usageCommandKind, `Auth status command failed: ${formatErrorMessage(error)}.`);
524
565
  return combined || null;
525
566
  }
526
567
  })();
@@ -557,45 +598,58 @@ function parseClaudeAuthStatusSnapshot(output) {
557
598
  return null;
558
599
  }
559
600
  }
560
- async function readClaudeUserIdHash(root, usageCommandKind, override, agentName) {
561
- const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
601
+ async function readClaudeUserIdHash(root, usageCommandKind, override, agentName, traceLogger) {
602
+ const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
562
603
  const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
563
604
  if (!snapshot) {
564
605
  return null;
565
606
  }
566
607
  return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
567
608
  }
568
- async function readClaudeUsageCommandOutput(root, usageCommandKind, override) {
609
+ async function readClaudeUsageCommandOutput(root, usageCommandKind, override, traceLogger) {
569
610
  if (override) {
570
611
  try {
571
- return await override();
612
+ const output = await override();
613
+ traceClaude(traceLogger, usageCommandKind, "Using injected /usage output override.");
614
+ traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(output)}`);
615
+ return output;
572
616
  }
573
617
  catch {
618
+ traceClaude(traceLogger, usageCommandKind, "Injected /usage output override failed.");
574
619
  return null;
575
620
  }
576
621
  }
577
622
  const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
578
623
  const cached = claudeUsageOutputCache.get(cacheKey);
579
624
  if (cached) {
625
+ traceClaude(traceLogger, usageCommandKind, "Usage output cache hit.");
580
626
  return cached;
581
627
  }
582
628
  const pending = (async () => {
583
- const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
629
+ const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
584
630
  if (!binaryPath) {
631
+ traceClaude(traceLogger, usageCommandKind, "Skipping /usage command because no Claude binary was found.");
632
+ traceClaude(traceLogger, usageCommandKind, "Usage returned:\n<not available>");
585
633
  return null;
586
634
  }
587
635
  try {
636
+ traceClaude(traceLogger, usageCommandKind, `Running /usage command with ${binaryPath} (TZ=UTC).`);
588
637
  const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
589
638
  encoding: "utf8",
639
+ env: buildClaudeCommandEnvironment(),
590
640
  maxBuffer: 1024 * 1024,
591
641
  timeout: 15000,
592
642
  windowsHide: true
593
643
  });
594
644
  const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
645
+ traceClaude(traceLogger, usageCommandKind, "Usage command completed successfully.");
646
+ traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(combined || null)}`);
595
647
  return combined || null;
596
648
  }
597
649
  catch (error) {
598
650
  const combined = extractExecOutput(error);
651
+ traceClaude(traceLogger, usageCommandKind, `Usage command failed: ${formatErrorMessage(error)}.`);
652
+ traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(combined || null)}`);
599
653
  return combined || null;
600
654
  }
601
655
  })();
@@ -610,51 +664,68 @@ function extractExecOutput(error) {
610
664
  const stderr = typeof error.stderr === "string" ? error.stderr : "";
611
665
  return [stdout, stderr].filter(Boolean).join("\n").trim();
612
666
  }
613
- async function resolveClaudeBinaryPath(root, usageCommandKind) {
667
+ async function resolveClaudeBinaryPath(root, usageCommandKind, traceLogger) {
614
668
  const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
615
669
  const cached = claudeBinaryPathCache.get(cacheKey);
616
670
  if (cached) {
617
- return cached;
671
+ const binaryPath = await cached;
672
+ traceClaude(traceLogger, usageCommandKind, `Binary detection cache hit: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
673
+ return binaryPath;
618
674
  }
619
- const pending = usageCommandKind === "vscode"
620
- ? resolveVsCodeClaudeBinaryPath(root)
621
- : resolveCliClaudeBinaryPath(root);
675
+ const pending = (async () => {
676
+ traceClaude(traceLogger, usageCommandKind, `Starting binary detection under ${root}.`);
677
+ const binaryPath = usageCommandKind === "vscode"
678
+ ? await resolveVsCodeClaudeBinaryPath(root, traceLogger)
679
+ : await resolveCliClaudeBinaryPath(root, traceLogger);
680
+ traceClaude(traceLogger, usageCommandKind, `Binary detection result: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
681
+ return binaryPath;
682
+ })();
622
683
  claudeBinaryPathCache.set(cacheKey, pending);
623
684
  return pending;
624
685
  }
625
- async function resolveVsCodeClaudeBinaryPath(root) {
686
+ async function resolveVsCodeClaudeBinaryPath(root, traceLogger) {
626
687
  const boosterDirectories = [
627
688
  path.join(root, ".vscode", "extensions"),
628
689
  path.join(root, ".vscode-server", "extensions"),
629
690
  path.join(root, ".vscode-server-insiders", "extensions")
630
691
  ];
692
+ let firstFoundPath = null;
631
693
  for (const directory of boosterDirectories) {
632
- const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory);
633
- if (binaryPath) {
634
- return binaryPath;
694
+ const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger);
695
+ if (!firstFoundPath && binaryPath) {
696
+ firstFoundPath = binaryPath;
635
697
  }
636
698
  }
637
- return null;
699
+ return firstFoundPath;
638
700
  }
639
- async function resolveClaudeBinaryFromExtensionDirectory(directory) {
701
+ async function resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger) {
640
702
  let entries;
703
+ traceClaude(traceLogger, "vscode", `Scanning extension directory ${directory}.`);
641
704
  try {
642
705
  entries = await fs.promises.readdir(directory, { withFileTypes: true });
643
706
  }
644
- catch {
707
+ catch (error) {
708
+ traceClaude(traceLogger, "vscode", `Could not read ${directory}: ${formatErrorMessage(error)}.`);
645
709
  return null;
646
710
  }
647
711
  const candidates = entries
648
712
  .filter((entry) => entry.isDirectory() && entry.name.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX))
649
713
  .map((entry) => entry.name)
650
714
  .sort(compareClaudeExtensionDirectoryNames);
715
+ if (candidates.length === 0) {
716
+ traceClaude(traceLogger, "vscode", `No Claude VSCode extension candidates found in ${directory}.`);
717
+ return null;
718
+ }
719
+ let firstFoundPath = null;
651
720
  for (const candidate of candidates) {
652
721
  const binaryPath = path.join(directory, candidate, "resources", "native-binary", "claude");
653
- if (await isReadableExecutableFile(binaryPath)) {
654
- return binaryPath;
722
+ const accessCheck = await checkReadableExecutableFile(binaryPath);
723
+ traceClaude(traceLogger, "vscode", `Checked ${binaryPath} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
724
+ if (!firstFoundPath && accessCheck.ok) {
725
+ firstFoundPath = binaryPath;
655
726
  }
656
727
  }
657
- return null;
728
+ return firstFoundPath;
658
729
  }
659
730
  function compareClaudeExtensionDirectoryNames(left, right) {
660
731
  const leftVersion = extractClaudeExtensionVersion(left);
@@ -678,25 +749,31 @@ function extractClaudeExtensionVersion(directoryName) {
678
749
  .map((part) => Number(part))
679
750
  .filter((part) => Number.isFinite(part));
680
751
  }
681
- async function resolveCliClaudeBinaryPath(root) {
752
+ async function resolveCliClaudeBinaryPath(root, traceLogger) {
682
753
  const directCandidates = [
683
754
  path.join(root, ".local", "bin", "claude"),
684
755
  path.join(root, "bin", "claude")
685
756
  ];
757
+ let firstFoundPath = null;
686
758
  for (const candidate of directCandidates) {
687
- if (await isReadableExecutableFile(candidate)) {
688
- return candidate;
759
+ const accessCheck = await checkReadableExecutableFile(candidate);
760
+ traceClaude(traceLogger, "cli", `Checked ${candidate} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
761
+ if (!firstFoundPath && accessCheck.ok) {
762
+ firstFoundPath = candidate;
689
763
  }
690
764
  }
691
- return null;
765
+ return firstFoundPath;
692
766
  }
693
- async function isReadableExecutableFile(filePath) {
767
+ async function checkReadableExecutableFile(filePath) {
694
768
  try {
695
769
  await fs.promises.access(filePath, fs.constants.R_OK | fs.constants.X_OK);
696
- return true;
770
+ return { ok: true };
697
771
  }
698
- catch {
699
- return false;
772
+ catch (error) {
773
+ return {
774
+ ok: false,
775
+ errorMessage: formatErrorMessage(error)
776
+ };
700
777
  }
701
778
  }
702
779
  function parseLiveUsageWindowSnapshots(usageOutput, now) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",