letmecode 0.1.5 → 0.1.6

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.
@@ -58,7 +58,8 @@ 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
64
  const userIdHash = await readClaudeUserIdHash(this.root, this.usageCommandKind, this.readAuthStatusOutput, agentName);
64
65
  const byModel = new Map();
@@ -149,7 +150,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
149
150
  totals: summaryTotals,
150
151
  distinctModels: modelUsage.map((row) => row.modelId),
151
152
  distinctPlanTypes: [...planTypes].sort(),
152
- rootLabel: "~/.claude/projects",
153
+ rootLabel: resolvedSessionsRoot.rootLabel,
153
154
  rootPath: sessionsRoot
154
155
  },
155
156
  modelUsage,
@@ -236,7 +237,87 @@ function resolveClaudeCacheWriteBreakdown(usage) {
236
237
  };
237
238
  }
238
239
  function isSessionFile(filePath) {
239
- return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
240
+ return filePath.endsWith(".jsonl");
241
+ }
242
+ async function resolveClaudeSessionsRoot(root) {
243
+ const candidates = buildClaudeSessionsRootCandidates(root);
244
+ for (const candidate of candidates) {
245
+ if (await isDirectory(candidate.rootPath)) {
246
+ return candidate;
247
+ }
248
+ }
249
+ return candidates[0] ?? {
250
+ rootLabel: "~/.claude/projects",
251
+ rootPath: path.join(path.resolve(root), ".claude", "projects")
252
+ };
253
+ }
254
+ function buildClaudeSessionsRootCandidates(root) {
255
+ const resolvedRoot = path.resolve(root);
256
+ const baseName = path.basename(resolvedRoot);
257
+ const parentBaseName = path.basename(path.dirname(resolvedRoot));
258
+ const candidates = [];
259
+ if (baseName === "projects") {
260
+ if (parentBaseName === ".claude") {
261
+ candidates.push({
262
+ rootLabel: "~/.claude/projects",
263
+ rootPath: resolvedRoot
264
+ });
265
+ }
266
+ else if (parentBaseName === "claude" || parentBaseName === "Claude") {
267
+ candidates.push({
268
+ rootLabel: `~/.config/${parentBaseName}/projects`,
269
+ rootPath: resolvedRoot
270
+ });
271
+ }
272
+ else {
273
+ candidates.push({
274
+ rootLabel: "projects",
275
+ rootPath: resolvedRoot
276
+ });
277
+ }
278
+ }
279
+ if (baseName === ".claude") {
280
+ candidates.push({
281
+ rootLabel: "~/.claude/projects",
282
+ rootPath: path.join(resolvedRoot, "projects")
283
+ });
284
+ }
285
+ if (parentBaseName === ".config" && (baseName === "claude" || baseName === "Claude")) {
286
+ candidates.push({
287
+ rootLabel: `~/.config/${baseName}/projects`,
288
+ rootPath: path.join(resolvedRoot, "projects")
289
+ });
290
+ }
291
+ candidates.push({
292
+ rootLabel: "~/.claude/projects",
293
+ rootPath: path.join(resolvedRoot, ".claude", "projects")
294
+ }, {
295
+ rootLabel: "~/.config/claude/projects",
296
+ rootPath: path.join(resolvedRoot, ".config", "claude", "projects")
297
+ }, {
298
+ rootLabel: "~/.config/Claude/projects",
299
+ rootPath: path.join(resolvedRoot, ".config", "Claude", "projects")
300
+ });
301
+ const dedupedCandidates = new Map();
302
+ for (const candidate of candidates) {
303
+ const normalizedPath = path.resolve(candidate.rootPath);
304
+ if (!dedupedCandidates.has(normalizedPath)) {
305
+ dedupedCandidates.set(normalizedPath, {
306
+ rootLabel: candidate.rootLabel,
307
+ rootPath: normalizedPath
308
+ });
309
+ }
310
+ }
311
+ return [...dedupedCandidates.values()];
312
+ }
313
+ async function isDirectory(directoryPath) {
314
+ try {
315
+ const stats = await fs.promises.stat(directoryPath);
316
+ return stats.isDirectory();
317
+ }
318
+ catch {
319
+ return false;
320
+ }
240
321
  }
241
322
  async function* walkSessionFiles(directory) {
242
323
  let entries;
@@ -544,7 +625,8 @@ async function resolveClaudeBinaryPath(root, usageCommandKind) {
544
625
  async function resolveVsCodeClaudeBinaryPath(root) {
545
626
  const boosterDirectories = [
546
627
  path.join(root, ".vscode", "extensions"),
547
- path.join(root, ".vscode-server", "extensions")
628
+ path.join(root, ".vscode-server", "extensions"),
629
+ path.join(root, ".vscode-server-insiders", "extensions")
548
630
  ];
549
631
  for (const directory of boosterDirectories) {
550
632
  const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory);
@@ -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,9 +1,10 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",
7
8
  "type": "commonjs",
8
9
  "bin": {
9
10
  "letmecode": "./bin/letmecode.js"
@@ -20,6 +21,16 @@
20
21
  "publishConfig": {
21
22
  "access": "public"
22
23
  },
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
+ },
23
34
  "keywords": [
24
35
  "cli",
25
36
  "ink",
@@ -36,14 +47,5 @@
36
47
  "@types/node": "^24.0.7",
37
48
  "@types/react": "^18.3.24",
38
49
  "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"
48
50
  }
49
- }
51
+ }