ai-lens 0.8.119 → 0.8.120

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/.commithash CHANGED
@@ -1 +1 @@
1
- 8674f71
1
+ bf6f7d9
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  History of changes to the `ai-lens` CLI package on npm. New entries go on top. Format: `## X.Y.Z — YYYY-MM-DD`, followed by user-facing bullets.
4
4
 
5
+ ## 0.8.120 — 2026-07-03
6
+ - fix: session events are no longer misattributed to an unrelated repository when a tool touches a file outside the project — project refinement now only follows genuine nested sub-repos, so the `projects` filter and per-project stats stay correct
7
+ - feat(codex): Codex sessions now report per-call token usage, matching Claude Code / Cursor granularity
8
+ - feat(codex): Codex sessions now capture per-message assistant text for dialogue and search
9
+
5
10
  ## 0.8.119 — 2026-07-02
6
11
  - feat: team-memory injection now fires only on genuinely relevant prompts — the trigger matches distinctive terms of the memory title instead of an absolute score, cutting noise ~20x while keeping real matches
7
12
  - feat: the full team memory pool is delivered to the session cache (previously most team-tier memories could never surface), server-gated to clients on this version and newer
package/client/capture.js CHANGED
@@ -297,45 +297,20 @@ function maybeCleanStaleTranscriptOffsets() {
297
297
  }
298
298
 
299
299
  /**
300
- * Incrementally read only the NEW bytes appended to a Claude Code transcript
301
- * since the last time this function was called for that transcript, and
302
- * return ONE entry per real (non-synthetic) assistant API call in the delta.
300
+ * Read the NEW bytes appended to a JSONL transcript since the last call for
301
+ * this path, returning the complete newline-terminated lines in that delta.
302
+ * The intricate byte-offset / rotation / partial-line handling lives HERE, in
303
+ * one place, shared by the Claude Code (assistant-line) and Codex (token_count)
304
+ * token extractors so the two can't silently diverge.
303
305
  *
304
- * Each entry corresponds to a single Anthropic API call: a single assistant
305
- * line in the JSONL with its own usage record. A single user->agent turn can
306
- * produce many of these (tool-use loops), and the caller is expected to emit
307
- * one unified TokenUsage event per entry symmetric with Cursor's per-call
308
- * AgentResponse rows and Codex's per-call TokenUsage rows.
309
- *
310
- * Returns { calls, commitOffset }:
311
- * - `calls` is Array<{usage, model, timestamp, uuid}>:
312
- * • `usage` has Anthropic-named keys (input_tokens, output_tokens,
313
- * cache_read_input_tokens, cache_creation_input_tokens) for
314
- * buildTokenUsageRaw.
315
- * • `model` is the model from that specific assistant line.
316
- * • `timestamp` is the assistant line's own ISO timestamp if present, else
317
- * null (caller falls back to the Stop event's timestamp).
318
- * • `uuid` is the assistant line's `uuid` field (Claude Code transcripts
319
- * always include one). Used by the caller to derive a stable event_id
320
- * so concurrent Stop hook invocations can't double-emit a single API call.
321
- * - `commitOffset` is a no-arg function the caller MUST invoke ONLY after
322
- * successfully writing every TokenUsage event derived from `calls` into
323
- * the spool. If spool writes fail (or the caller never calls it), the
324
- * transcript cursor stays put and the next Stop re-reads the same delta —
325
- * the server-side ON CONFLICT on the deterministic event_id then dedups
326
- * whichever rows did land. This is what makes the pipeline crash-safe:
327
- * we never mark transcript bytes "consumed" until we know the rows they
328
- * produced are durably queued.
329
- *
330
- * Returns an empty `calls` array (and a no-op `commitOffset`) if nothing new
331
- * in the delta or no real assistant lines are found.
332
- *
333
- * Partial-line handling: a Stop hook can fire while Claude Code is mid-write,
334
- * so the delta may end before a trailing '\n'. We detect this, ignore the
335
- * partial tail, and persist the offset at the byte right after the LAST
336
- * complete '\n' so the next Stop re-reads the partial line once it's finished.
306
+ * Returns { lines, commitOffset }:
307
+ * - `lines` raw JSONL lines in the processable region (empty on nothing-new
308
+ * or any I/O failure).
309
+ * - `commitOffset` a no-arg fn the caller MUST invoke ONLY after it has
310
+ * durably consumed every line (advancing the cursor eagerly would drop rows
311
+ * if a later spool write fails).
337
312
  */
338
- function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
313
+ function readNewTranscriptDelta(transcriptPath) {
339
314
  // Run the rate-limited stale-offset cleanup at most once per 24h. Wrapped in
340
315
  // try/catch so a cleanup failure never prevents the token read.
341
316
  try { maybeCleanStaleTranscriptOffsets(); } catch { /* best-effort */ }
@@ -345,7 +320,7 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
345
320
  const noopCommit = () => {};
346
321
 
347
322
  if (!transcriptPath || typeof transcriptPath !== 'string') {
348
- return { calls: [], commitOffset: noopCommit };
323
+ return { lines: [], commitOffset: noopCommit };
349
324
  }
350
325
 
351
326
  const offsetFile = join(TRANSCRIPT_OFFSETS_DIR, encodeURIComponent(transcriptPath));
@@ -358,7 +333,7 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
358
333
  currentMtime = st.mtimeMs;
359
334
  } catch (err) {
360
335
  captureLog({ msg: 'transcript-offset-error', stage: 'stat', path: transcriptPath, error: err?.message });
361
- return { calls: [], commitOffset: noopCommit };
336
+ return { lines: [], commitOffset: noopCommit };
362
337
  }
363
338
 
364
339
  const savedState = readTranscriptOffsetState(offsetFile);
@@ -377,7 +352,7 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
377
352
 
378
353
  // Nothing new — just a stat, super cheap.
379
354
  if (currentSize === startOffset) {
380
- return { calls: [], commitOffset: noopCommit };
355
+ return { lines: [], commitOffset: noopCommit };
381
356
  }
382
357
 
383
358
  let buffer = null;
@@ -395,13 +370,13 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
395
370
  // Read failed before we got any bytes — leave the cursor unchanged so the
396
371
  // next Stop retries. (Old behavior advanced past the unread bytes; that
397
372
  // silently dropped data on transient I/O errors.)
398
- return { calls: [], commitOffset: noopCommit };
373
+ return { lines: [], commitOffset: noopCommit };
399
374
  }
400
375
 
401
376
  if (!buffer || buffer.length === 0) {
402
377
  // Defensive: shouldn't happen because of the currentSize === startOffset
403
378
  // early return above. Don't advance the cursor.
404
- return { calls: [], commitOffset: noopCommit };
379
+ return { lines: [], commitOffset: noopCommit };
405
380
  }
406
381
 
407
382
  // Find the LAST complete newline. CRITICAL: search the BYTE buffer, not a
@@ -416,7 +391,7 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
416
391
  if (lastNewlineIndex === -1) {
417
392
  // The entire chunk is a partial trailing line — leave the cursor unchanged
418
393
  // so the next Stop re-reads the line once it's finished.
419
- return { calls: [], commitOffset: noopCommit };
394
+ return { lines: [], commitOffset: noopCommit };
420
395
  }
421
396
 
422
397
  // Process only the bytes up to (and including) the last '\n'. The new
@@ -428,12 +403,36 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
428
403
  const nextState = { offset: newOffset, size: currentSize, mtime_ms: currentMtime };
429
404
  const commitOffset = () => writeTranscriptOffsetState(offsetFile, nextState);
430
405
 
406
+ return { lines: processable.split('\n'), commitOffset };
407
+ }
408
+
409
+ /**
410
+ * Incrementally read only the NEW bytes appended to a Claude Code transcript
411
+ * since the last time this function was called for that transcript, and
412
+ * return ONE entry per real (non-synthetic) assistant API call in the delta.
413
+ *
414
+ * Each entry corresponds to a single Anthropic API call: a single assistant
415
+ * line in the JSONL with its own usage record. A single user->agent turn can
416
+ * produce many of these (tool-use loops), and the caller is expected to emit
417
+ * one unified TokenUsage event per entry — symmetric with Cursor's per-call
418
+ * AgentResponse rows and Codex's per-call TokenUsage rows.
419
+ *
420
+ * Returns { calls, commitOffset } (see readNewTranscriptDelta for the
421
+ * commit-offset crash-safety contract): `calls` is Array<{usage, model,
422
+ * timestamp, uuid}> with Anthropic-named usage keys for buildTokenUsageRaw,
423
+ * the model + ISO timestamp from that specific assistant line, and the line's
424
+ * `uuid` (used by the caller to derive a stable event_id so concurrent Stop
425
+ * hook invocations can't double-emit a single API call).
426
+ */
427
+ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
428
+ const { lines, commitOffset } = readNewTranscriptDelta(transcriptPath);
429
+ if (!lines.length) return { calls: [], commitOffset };
430
+
431
431
  // Iterate lines in the processable region. Defensive parse-tolerance is
432
432
  // still useful for the rare case where a single line in the middle is
433
433
  // malformed (we skip it rather than dropping the whole delta).
434
434
  const calls = [];
435
435
  try {
436
- const lines = processable.split('\n');
437
436
  for (const rawLine of lines) {
438
437
  const line = rawLine && rawLine.trim();
439
438
  if (!line) continue;
@@ -493,7 +492,7 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
493
492
  // to spool in this path, so it's safe to commit immediately (no risk of
494
493
  // losing events).
495
494
  commitOffset();
496
- return { calls: [], commitOffset: noopCommit };
495
+ return { calls: [], commitOffset: () => {} };
497
496
  }
498
497
 
499
498
  // IMPORTANT: do NOT persist the offset here. The caller is responsible for
@@ -504,6 +503,173 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
504
503
  return { calls, commitOffset };
505
504
  }
506
505
 
506
+ /**
507
+ * Codex analogue of extractNewClaudeApiCallsFromTranscript: read the NEW bytes
508
+ * appended to a Codex rollout transcript (`rollout-*.jsonl`) since the last
509
+ * call and, in a SINGLE delta pass, return both the per-call token usage
510
+ * (`calls`) and the assistant's dialogue text (`messages`).
511
+ *
512
+ * A single pass is mandatory: `readNewTranscriptDelta` advances a per-path byte
513
+ * cursor that is committed only after the caller spools everything, so a second
514
+ * independent read would re-consume the same bytes. Claude's extractor folds
515
+ * text into each `call` (usage + content share one assistant line); Codex keeps
516
+ * them separate because usage and text live in DIFFERENT interleaved records.
517
+ *
518
+ * TOKEN USAGE (`calls`) — Codex records per-call usage as `event_msg`/
519
+ * `token_count` records whose `payload.info.last_token_usage` is the DELTA for
520
+ * that single call (the sibling `total_token_usage` is the running cumulative
521
+ * sum). The rollout is OpenAI-shaped, so field names differ from Claude's and
522
+ * we normalize them to the Anthropic convention that buildTokenUsageRaw + the
523
+ * dashboard SQL expect:
524
+ * - Codex `cached_input_tokens` ⊆ `input_tokens` (a subset), so we split them
525
+ * into non-overlapping `input_tokens` (fresh) + `cache_read_input_tokens`
526
+ * (cached), matching Claude where `input_tokens` excludes the cache read.
527
+ * - Codex has no prompt-cache-creation notion → `cache_creation_input_tokens`
528
+ * is always 0.
529
+ * - `output_tokens` already includes `reasoning_output_tokens`, mirroring
530
+ * Claude's output convention, so it passes through unchanged.
531
+ * Records whose call delta is all-zero (input+output == 0, e.g. the residual
532
+ * token_count emitted around a compaction) are skipped so we never spool an
533
+ * empty TokenUsage row.
534
+ *
535
+ * ASSISTANT TEXT (`messages`, ANL-1239) — the assistant's visible dialogue
536
+ * lives in `response_item`/`message` records with `role === 'assistant'`; we
537
+ * concatenate their `content[]` `output_text` blocks. The dedup key is
538
+ * `${timestamp}:${payload.id | sha(text)}`: current Codex writes a durable
539
+ * `msg_…` id on every assistant message (hard version cutoff ~2026-06-26; older
540
+ * rollouts have none), so we prefer it and fall back to a text fingerprint —
541
+ * mirroring how the token path synthesizes a stable key from records that lack
542
+ * a per-record uuid. The
543
+ * caller emits these as AssistantText events, metric-neutral server-side
544
+ * (ASSISTANT_CONTENT_TYPES). NOTE: there is deliberately no AssistantThinking
545
+ * analogue (unlike Claude). Current Codex emits no plaintext reasoning: the
546
+ * full chain-of-thought lives only in `response_item`/`reasoning`
547
+ * `encrypted_content`, and the short plaintext `summary` those records once
548
+ * carried was dropped ~2026-04 (verified on real rollouts: 91% of Feb reasoning
549
+ * records had a summary, 0% from April on). Since AI Lens captures live/current
550
+ * Codex, no recoverable thinking text exists.
551
+ *
552
+ * The model isn't on the token_count / message record; it lives on the nearest
553
+ * preceding `turn_context` record. We track the last-seen turn_context model
554
+ * within the delta and fall back to `fallbackModel` (the Stop event's model)
555
+ * when the governing turn_context landed in an earlier delta.
556
+ *
557
+ * @param {string} transcriptPath Path to the Codex rollout JSONL.
558
+ * @param {string|null} fallbackModel Model from the Stop event, used when no
559
+ * turn_context precedes a record in this
560
+ * delta.
561
+ * @returns {{calls: Array<{usage, model, timestamp, uuid}>,
562
+ * messages: Array<{text, model, timestamp, uuid}>,
563
+ * commitOffset: function}}
564
+ */
565
+ function extractNewCodexRecordsFromTranscript(transcriptPath, fallbackModel) {
566
+ const { lines, commitOffset } = readNewTranscriptDelta(transcriptPath);
567
+ if (!lines.length) return { calls: [], messages: [], commitOffset };
568
+
569
+ const calls = [];
570
+ const messages = [];
571
+ let currentModel = fallbackModel || null;
572
+ try {
573
+ for (const rawLine of lines) {
574
+ const line = rawLine && rawLine.trim();
575
+ if (!line) continue;
576
+ let parsed;
577
+ try {
578
+ parsed = JSON.parse(line);
579
+ } catch {
580
+ continue;
581
+ }
582
+ // A turn_context record carries the model for the records that follow it.
583
+ if (parsed?.type === 'turn_context' && typeof parsed.payload?.model === 'string' && parsed.payload.model) {
584
+ currentModel = parsed.payload.model;
585
+ continue;
586
+ }
587
+ const payload = parsed?.payload;
588
+ if (!payload) continue;
589
+ const ts = typeof parsed.timestamp === 'string' ? parsed.timestamp : null;
590
+
591
+ // Assistant dialogue text — response_item/message with role 'assistant'.
592
+ if (parsed.type === 'response_item' && payload.type === 'message' && payload.role === 'assistant') {
593
+ let text = '';
594
+ if (Array.isArray(payload.content)) {
595
+ for (const block of payload.content) {
596
+ if (!block || typeof block !== 'object') continue;
597
+ // Assistant content is 'output_text'; accept 'text' too, defensively.
598
+ if ((block.type === 'output_text' || block.type === 'text') && typeof block.text === 'string' && block.text) {
599
+ text += (text ? '\n' : '') + block.text;
600
+ }
601
+ }
602
+ }
603
+ if (text) {
604
+ // Stable per-message dedup key: `${timestamp}:${id | sha(text)}`.
605
+ // Current Codex writes a durable `msg_…` id on every assistant
606
+ // message (verified: 100% from the 2026-06-26 build onward; older
607
+ // rollouts, ≤ mid-May in local data, carry none — a hard version
608
+ // cutoff, not a gradual mix). Live capture only ever sees current
609
+ // rollouts, so the id is normally present; the text-hash fallback is
610
+ // defensive insurance for a pre-cutoff / downgraded client. Either
611
+ // way the key is unique per record and identical across two Stop
612
+ // hooks reading the same bytes (main() re-hashes source_uuid into the
613
+ // event_id). Prefer the durable id so the key stays human-traceable.
614
+ const idPart = typeof payload.id === 'string' && payload.id
615
+ ? payload.id
616
+ : createHash('sha256').update(text).digest('hex').slice(0, 16);
617
+ const uuid = `${ts || ''}:${idPart}`;
618
+ messages.push({
619
+ text,
620
+ model: currentModel,
621
+ timestamp: ts,
622
+ uuid,
623
+ });
624
+ }
625
+ continue;
626
+ }
627
+
628
+ if (payload.type !== 'token_count') continue;
629
+ const info = payload.info;
630
+ const last = info?.last_token_usage;
631
+ if (!last || typeof last !== 'object') continue;
632
+
633
+ const rawInput = toNumberOrNull(last.input_tokens) ?? 0;
634
+ const cached = toNumberOrNull(last.cached_input_tokens) ?? 0;
635
+ const output = toNumberOrNull(last.output_tokens) ?? 0;
636
+ // Skip all-zero deltas (residual token_count around a compaction).
637
+ if (rawInput + output <= 0) continue;
638
+
639
+ // cached_input_tokens is a SUBSET of input_tokens — split into
640
+ // non-overlapping fresh-input + cache-read to match Claude's convention.
641
+ const cacheRead = Math.min(cached, rawInput);
642
+ const freshInput = Math.max(0, rawInput - cacheRead);
643
+
644
+ // Stable per-call fingerprint for the dedup event_id. The token_count
645
+ // record has no per-record uuid, so we key on the record timestamp + the
646
+ // cumulative total (monotonic across calls) — unique per token_count
647
+ // record and identical across two concurrent Stop hooks reading the same
648
+ // bytes.
649
+ const cum = toNumberOrNull(info?.total_token_usage?.total_tokens);
650
+ const uuid = `${ts || ''}:${cum != null ? cum : ''}`;
651
+
652
+ calls.push({
653
+ usage: {
654
+ input_tokens: freshInput,
655
+ output_tokens: output,
656
+ cache_read_input_tokens: cacheRead,
657
+ cache_creation_input_tokens: 0,
658
+ },
659
+ model: currentModel,
660
+ timestamp: ts,
661
+ uuid: uuid === ':' ? null : uuid,
662
+ });
663
+ }
664
+ } catch (err) {
665
+ captureLog({ msg: 'transcript-offset-error', stage: 'parse-codex', path: transcriptPath, error: err?.message });
666
+ commitOffset();
667
+ return { calls: [], messages: [], commitOffset: () => {} };
668
+ }
669
+
670
+ return { calls, messages, commitOffset };
671
+ }
672
+
507
673
  // =============================================================================
508
674
  // Source Detection
509
675
  // =============================================================================
@@ -687,7 +853,14 @@ function refineProjectPath(event, projectPath, sessionId) {
687
853
  const filePath = extractFilePath(toolInput);
688
854
  if (!filePath) return projectPath;
689
855
  const gitRoot = findGitRoot(filePath);
690
- if (gitRoot && (!projectPath || gitRoot.length > projectPath.length)) {
856
+ // Refine ONLY downward into the current project itself or a real sub-repo
857
+ // of it (nested repos like etl-pipelines/bk-reports-automation/). Never flip
858
+ // to an unrelated repo just because its path string happens to be longer:
859
+ // a session running in one project that touches a file in a sibling/foreign
860
+ // repo would otherwise get misattributed there (and mis-filtered against the
861
+ // `projects` config). `pathContains` handles the nesting check plus Windows
862
+ // backslash/drive-letter/case normalization for free.
863
+ if (gitRoot && (!projectPath || pathContains(projectPath, gitRoot))) {
691
864
  if (sessionId) cacheSessionPath(sessionId, gitRoot);
692
865
  return gitRoot;
693
866
  }
@@ -1369,7 +1542,7 @@ function normalizeCodex(event) {
1369
1542
  projectPath = refineProjectPath({ tool_input: codexToolInput(event), input: codexToolInput(event) }, projectPath, sessionId);
1370
1543
  }
1371
1544
 
1372
- return [{
1545
+ const primary = {
1373
1546
  event_id: null,
1374
1547
  source: 'codex',
1375
1548
  session_id: sessionId,
@@ -1378,7 +1551,59 @@ function normalizeCodex(event) {
1378
1551
  timestamp,
1379
1552
  data,
1380
1553
  raw: event,
1381
- }];
1554
+ };
1555
+
1556
+ // On Stop (end of a Codex agent turn), read the rollout delta once and emit
1557
+ // one TokenUsage event per real API call PLUS one AssistantText event per
1558
+ // assistant message (ANL-1239) — giving Codex the same row-per-call
1559
+ // granularity as Claude Code and Cursor. The rollout path arrives as
1560
+ // `transcript_path` (the same field detectSource keys on for `rollout-*`).
1561
+ if (hookType === 'Stop') {
1562
+ const transcriptPath = event.transcript_path || null;
1563
+ const fallbackModel = typeof event.model === 'string' ? event.model : null;
1564
+ const { calls, messages, commitOffset } = extractNewCodexRecordsFromTranscript(transcriptPath, fallbackModel);
1565
+ const tokenEvents = calls.map(call => ({
1566
+ event_id: null, // assigned in main() from a stable hash of call.uuid
1567
+ source: 'codex',
1568
+ session_id: sessionId,
1569
+ type: 'TokenUsage',
1570
+ project_path: projectPath,
1571
+ timestamp: call.timestamp || timestamp,
1572
+ data: {
1573
+ model: call.model,
1574
+ input_tokens: call.usage.input_tokens,
1575
+ output_tokens: call.usage.output_tokens,
1576
+ },
1577
+ raw: buildTokenUsageRaw({ source_uuid: call.uuid }, call.usage, call.model),
1578
+ }));
1579
+ // Assistant dialogue text (ANL-1239). Stored for dialogue/search;
1580
+ // metric-neutral server-side (ASSISTANT_CONTENT_TYPES). data.text is
1581
+ // truncated for display; raw.text keeps the full text (redacted
1582
+ // server-side). Codex has NO plaintext thinking, so there is no
1583
+ // AssistantThinking counterpart (unlike the Claude Code Stop path).
1584
+ const contentEvents = messages.map(msg => ({
1585
+ event_id: null, // assigned in main() from a stable hash of msg.uuid
1586
+ source: 'codex',
1587
+ session_id: sessionId,
1588
+ type: 'AssistantText',
1589
+ project_path: projectPath,
1590
+ timestamp: msg.timestamp || timestamp,
1591
+ data: { text: truncate(msg.text, TRUNCATION_LIMITS.agentResponse), model: msg.model },
1592
+ raw: { source_uuid: msg.uuid, model: msg.model, text: msg.text },
1593
+ }));
1594
+ const result = [primary, ...tokenEvents, ...contentEvents];
1595
+ // Attach the commit callback as a non-enumerable property so it survives
1596
+ // through normalizeEvent()/writeToSpool without leaking into iteration or
1597
+ // length assertions — mirrors the Claude Code Stop path.
1598
+ Object.defineProperty(result, 'commitTranscriptOffset', {
1599
+ value: commitOffset,
1600
+ enumerable: false,
1601
+ writable: false,
1602
+ });
1603
+ return result;
1604
+ }
1605
+
1606
+ return [primary];
1382
1607
  }
1383
1608
 
1384
1609
  // =============================================================================
@@ -2142,7 +2367,7 @@ async function main() {
2142
2367
  : ev.type === 'AssistantThinking' ? 'assistantthinking'
2143
2368
  : 'tokenusage';
2144
2369
  if (sourceUuid) {
2145
- ev.event_id = deterministicEventId(`claude_code:${kind}:${sourceUuid}`);
2370
+ ev.event_id = deterministicEventId(`${ev.source}:${kind}:${sourceUuid}`);
2146
2371
  } else {
2147
2372
  // Fallback: stdin hash + per-event index. Should never be needed because
2148
2373
  // Claude Code transcripts always include uuid per record.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.119",
3
+ "version": "0.8.120",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {