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 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
@@ -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 sessionsRoot = path.join(this.root, ".claude", "projects");
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: "~/.claude/projects",
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") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
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
- return parseClaudeSubscriptionType(output);
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
- return await override();
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
- return await override();
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
- return cached;
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 = usageCommandKind === "vscode"
539
- ? resolveVsCodeClaudeBinaryPath(root)
540
- : resolveCliClaudeBinaryPath(root);
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
- return binaryPath;
686
+ const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger);
687
+ if (!firstFoundPath && binaryPath) {
688
+ firstFoundPath = binaryPath;
553
689
  }
554
690
  }
555
- return null;
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
- if (await isReadableExecutableFile(binaryPath)) {
572
- return binaryPath;
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 null;
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
- if (await isReadableExecutableFile(candidate)) {
606
- return candidate;
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 null;
757
+ return firstFoundPath;
610
758
  }
611
- async function isReadableExecutableFile(filePath) {
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 false;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",