letmecode 0.1.10 → 0.1.11

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.
@@ -78,13 +78,14 @@ export class ClaudeUsageProvider extends UsageProviderBase {
78
78
  const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot, this.usageCommandKind, options.traceLogger);
79
79
  traceClaude(options.traceLogger, this.usageCommandKind, `Loaded ${parsedSessionFiles.length} parsed session file(s) from ${sessionsRoot}.`);
80
80
  for (const file of parsedSessionFiles) {
81
- const matchingEvents = file.events.filter((event) => this.entrypoints.has(event.entrypoint));
81
+ const matchingEvents = file.events.filter((event) => matchesClaudeProviderEvent(event, file, this.entrypoints, this.usageCommandKind));
82
82
  traceClaude(options.traceLogger, this.usageCommandKind, [
83
83
  `Session file ${describeSessionFilePath(sessionsRoot, file.filePath)}:`,
84
84
  `lines=${file.linesRead}`,
85
85
  `malformed=${file.malformedLines}`,
86
86
  `assistantUsageEvents=${file.events.length}`,
87
87
  `matchingEvents=${matchingEvents.length}`,
88
+ `source=${file.sourceKind}`,
88
89
  `entrypoints=${summarizeEventCounts(file.events.map((event) => event.entrypoint || "<empty>"))}`,
89
90
  `models=${summarizeDistinctValues(file.events.map((event) => event.modelId || "unknown"))}`
90
91
  ].join(" "));
@@ -390,20 +391,26 @@ async function loadParsedClaudeSessionFiles(sessionsRoot, usageCommandKind, trac
390
391
  const files = [];
391
392
  traceClaude(traceLogger, usageCommandKind, `Scanning session files under ${sessionsRoot}.`);
392
393
  for await (const filePath of walkSessionFiles(sessionsRoot)) {
393
- files.push(await parseSessionFile(filePath));
394
+ files.push(await parseSessionFile(filePath, sessionsRoot));
394
395
  }
396
+ inferClaudeSessionFileSources(files);
395
397
  traceClaude(traceLogger, usageCommandKind, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
396
398
  return files;
397
399
  })();
398
400
  parsedClaudeSessionFilesCache.set(cacheKey, pending);
399
401
  return pending;
400
402
  }
401
- async function parseSessionFile(filePath) {
403
+ async function parseSessionFile(filePath, sessionsRoot) {
402
404
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
403
405
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
404
406
  let linesRead = 0;
405
407
  let malformedLines = 0;
406
408
  const events = [];
409
+ const assistantEntryPoints = new Set();
410
+ let hasIdeOpenedFileAttachment = false;
411
+ let hasIdeOpenedFileMarker = false;
412
+ let hasIdeTooling = false;
413
+ let hasQueueOperations = false;
407
414
  for await (const line of lineReader) {
408
415
  linesRead += 1;
409
416
  if (!line.trim()) {
@@ -417,6 +424,19 @@ async function parseSessionFile(filePath) {
417
424
  malformedLines += 1;
418
425
  continue;
419
426
  }
427
+ if (payloadObject.type === "queue-operation") {
428
+ hasQueueOperations = true;
429
+ }
430
+ if (messageContainsIdeOpenedFileMarker(asRecord(payloadObject.message))) {
431
+ hasIdeOpenedFileMarker = true;
432
+ }
433
+ const attachment = asRecord(payloadObject.attachment);
434
+ if (attachment?.type === "opened_file_in_ide") {
435
+ hasIdeOpenedFileAttachment = true;
436
+ }
437
+ if (attachmentHasIdeTooling(attachment)) {
438
+ hasIdeTooling = true;
439
+ }
420
440
  if (payloadObject.type !== "assistant") {
421
441
  continue;
422
442
  }
@@ -427,12 +447,14 @@ async function parseSessionFile(filePath) {
427
447
  }
428
448
  const modelId = String(message?.model ?? "unknown");
429
449
  const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
450
+ const entrypoint = typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "";
430
451
  const rateLimits = extractRateLimits(payloadObject, message);
431
452
  const normalizedUsage = normalizeUsage(usage);
432
453
  const usageKey = buildUsageEventKey(payloadObject, message);
433
454
  const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
455
+ assistantEntryPoints.add(entrypoint);
434
456
  events.push({
435
- entrypoint: typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "",
457
+ entrypoint,
436
458
  usageKey,
437
459
  usageSignature,
438
460
  timestampMs: eventTimeMs,
@@ -441,7 +463,101 @@ async function parseSessionFile(filePath) {
441
463
  rateLimits
442
464
  });
443
465
  }
444
- return { filePath, linesRead, malformedLines, events };
466
+ return {
467
+ filePath,
468
+ sessionGroupKey: buildClaudeSessionGroupKey(sessionsRoot, filePath),
469
+ linesRead,
470
+ malformedLines,
471
+ sourceKind: "unknown",
472
+ sourceReason: "unclassified",
473
+ signals: {
474
+ assistantEntryPoints: [...assistantEntryPoints].sort(),
475
+ hasIdeOpenedFileAttachment,
476
+ hasIdeOpenedFileMarker,
477
+ hasIdeTooling,
478
+ hasQueueOperations
479
+ },
480
+ events
481
+ };
482
+ }
483
+ function buildClaudeSessionGroupKey(sessionsRoot, filePath) {
484
+ const relativePath = path.relative(sessionsRoot, filePath);
485
+ if (!relativePath || relativePath.startsWith("..")) {
486
+ return filePath;
487
+ }
488
+ const normalizedRelativePath = relativePath.split(path.sep).join("/");
489
+ const subagentMatch = normalizedRelativePath.match(/^(.*\/[^/]+)\/subagents\/[^/]+\.jsonl$/);
490
+ if (subagentMatch?.[1]) {
491
+ return subagentMatch[1];
492
+ }
493
+ return normalizedRelativePath.replace(/\.jsonl$/i, "");
494
+ }
495
+ function inferClaudeSessionFileSources(files) {
496
+ const groups = new Map();
497
+ for (const file of files) {
498
+ const group = groups.get(file.sessionGroupKey) ?? {
499
+ assistantEntryPoints: new Set(),
500
+ hasIdeHints: false
501
+ };
502
+ for (const entrypoint of file.signals.assistantEntryPoints) {
503
+ group.assistantEntryPoints.add(entrypoint);
504
+ }
505
+ group.hasIdeHints =
506
+ group.hasIdeHints ||
507
+ file.signals.hasIdeOpenedFileAttachment ||
508
+ file.signals.hasIdeOpenedFileMarker ||
509
+ file.signals.hasIdeTooling ||
510
+ file.signals.hasQueueOperations;
511
+ groups.set(file.sessionGroupKey, group);
512
+ }
513
+ for (const file of files) {
514
+ const group = groups.get(file.sessionGroupKey);
515
+ const { kind, reason } = classifyClaudeSessionGroup(group);
516
+ file.sourceKind = kind;
517
+ file.sourceReason = reason;
518
+ }
519
+ }
520
+ function classifyClaudeSessionGroup(group) {
521
+ if (!group) {
522
+ return { kind: "unknown", reason: "missing session group signals" };
523
+ }
524
+ if (group.assistantEntryPoints.has("claude-vscode")) {
525
+ return { kind: "vscode", reason: "explicit claude-vscode entrypoint" };
526
+ }
527
+ if (group.assistantEntryPoints.has("sdk-cli") || group.assistantEntryPoints.has("claude")) {
528
+ return { kind: "cli", reason: "explicit sdk-cli/claude entrypoint" };
529
+ }
530
+ if (group.assistantEntryPoints.has("cli")) {
531
+ return group.hasIdeHints
532
+ ? { kind: "vscode", reason: "generic cli entrypoint with IDE session hints" }
533
+ : { kind: "cli", reason: "generic cli entrypoint without IDE session hints" };
534
+ }
535
+ return { kind: "unknown", reason: "no assistant entrypoints" };
536
+ }
537
+ function attachmentHasIdeTooling(attachment) {
538
+ if (attachment?.type !== "deferred_tools_delta") {
539
+ return false;
540
+ }
541
+ return extractStringArray(attachment.addedNames).some((name) => name.startsWith("mcp__ide__"));
542
+ }
543
+ function messageContainsIdeOpenedFileMarker(message) {
544
+ const content = message?.content;
545
+ if (typeof content === "string") {
546
+ return content.includes("<ide_opened_file>");
547
+ }
548
+ if (!Array.isArray(content)) {
549
+ return false;
550
+ }
551
+ return content.some((item) => {
552
+ const contentItem = asRecord(item);
553
+ return typeof contentItem?.text === "string" && contentItem.text.includes("<ide_opened_file>");
554
+ });
555
+ }
556
+ function extractStringArray(value) {
557
+ if (!Array.isArray(value)) {
558
+ return [];
559
+ }
560
+ return value.filter((item) => typeof item === "string");
445
561
  }
446
562
  function buildUsageEventKey(payloadObject, message) {
447
563
  const sessionId = String(payloadObject.sessionId ?? "");
@@ -509,6 +625,18 @@ function shouldReplaceUsageEvent(previous, next) {
509
625
  }
510
626
  return false;
511
627
  }
628
+ function matchesClaudeProviderEvent(event, file, entrypoints, usageCommandKind) {
629
+ if (entrypoints.has(event.entrypoint)) {
630
+ return true;
631
+ }
632
+ if (event.entrypoint !== "cli") {
633
+ return false;
634
+ }
635
+ if (usageCommandKind === "vscode") {
636
+ return file.sourceKind === "vscode";
637
+ }
638
+ return file.sourceKind === "cli";
639
+ }
512
640
  function normalizeTimestamp(value) {
513
641
  return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
514
642
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",