ai-lens 0.8.112 → 0.8.113

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
- 1411e25
1
+ f10ad46
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
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.113 — 2026-06-24
6
+ - feat: track working-directory changes mid-session (Claude Code `CwdChanged`) so events are attributed to the project you `cd` into, not the one you started in
7
+ - feat: capture the assistant's response text and thinking from the Claude Code transcript, not just token counts — the full turn-by-turn dialogue is now recorded
8
+
5
9
  ## 0.8.112 — 2026-06-23
6
10
  - feat: re-running `init --yes` no longer changes your MCP registration. On an already-set-up install it's left exactly as-is — if MCP is registered it stays (same scope), if it isn't it's left off — instead of being removed-and-re-added at user scope (which could migrate its scope, force it on, or drop it if the re-add failed). Fresh installs still register MCP; use `init --mcp-only` to deliberately (re)register.
7
11
 
package/cli/hooks.js CHANGED
@@ -502,6 +502,11 @@ const CLAUDE_HOOK_SPEC = {
502
502
  TaskCompleted: { matcher: '' },
503
503
  InstructionsLoaded: { matcher: '' },
504
504
  UserPromptExpansion: { matcher: '' },
505
+ // Project attribution: fires when the session's cwd changes mid-flight.
506
+ // capture.js updates the session→project cache from new_cwd so subsequent
507
+ // events attribute to the right project (cwd otherwise only arrives at
508
+ // SessionStart). Metric-neutral server-side (OBSERVABILITY_EVENT_TYPES).
509
+ CwdChanged: { matcher: '' },
505
510
  };
506
511
 
507
512
  const CURSOR_HOOK_NAMES = [
package/client/capture.js CHANGED
@@ -419,6 +419,24 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
419
419
  if (!model || model === '<synthetic>') continue;
420
420
  const usage = message.usage;
421
421
  if (!usage || typeof usage !== 'object') continue;
422
+ // Also lift the assistant's own text + thinking blocks from this call's
423
+ // content. They live ONLY in the transcript (no hook payload carries the
424
+ // intermediate assistant text between tool calls), so the caller emits
425
+ // them as AssistantText / AssistantThinking content events alongside
426
+ // TokenUsage — closing the one dialogue gap. Concatenate multiple blocks
427
+ // of the same kind in order.
428
+ let text = '';
429
+ let thinking = '';
430
+ if (Array.isArray(message.content)) {
431
+ for (const block of message.content) {
432
+ if (!block || typeof block !== 'object') continue;
433
+ if (block.type === 'text' && typeof block.text === 'string' && block.text) {
434
+ text += (text ? '\n' : '') + block.text;
435
+ } else if (block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking) {
436
+ thinking += (thinking ? '\n' : '') + block.thinking;
437
+ }
438
+ }
439
+ }
422
440
  // Capture each call as its own entry — the caller emits one unified
423
441
  // TokenUsage event per entry, matching Cursor/Codex per-call granularity.
424
442
  calls.push({
@@ -431,6 +449,8 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
431
449
  model,
432
450
  timestamp: typeof parsed.timestamp === 'string' ? parsed.timestamp : null,
433
451
  uuid: typeof parsed.uuid === 'string' ? parsed.uuid : null,
452
+ text: text || null,
453
+ thinking: thinking || null,
434
454
  });
435
455
  }
436
456
  } catch (err) {
@@ -751,6 +771,22 @@ function normalizeClaudeCode(event) {
751
771
  case 'SessionStart':
752
772
  data = { cwd: event.cwd };
753
773
  break;
774
+ case 'CwdChanged': {
775
+ // The session's working dir changed mid-flight. cwd otherwise only
776
+ // arrives at SessionStart, so refresh the session→project cache from
777
+ // new_cwd and attribute THIS event to the new project. Canonicalizes
778
+ // worktree checkouts to the main repo root like SessionStart does.
779
+ const newCwd = event.new_cwd || event.cwd || null;
780
+ data = { old_cwd: event.old_cwd || null, new_cwd: newCwd };
781
+ if (newCwd) {
782
+ const newPath = canonicalizeProjectPath(newCwd);
783
+ if (newPath) {
784
+ projectPath = newPath;
785
+ if (sessionId) cacheSessionPath(sessionId, newPath);
786
+ }
787
+ }
788
+ break;
789
+ }
754
790
  case 'UserPromptSubmit':
755
791
  data = { prompt: truncate(event.prompt || '', TRUNCATION_LIMITS.userPrompt) };
756
792
  break;
@@ -933,7 +969,37 @@ function normalizeClaudeCode(event) {
933
969
  },
934
970
  raw: buildTokenUsageRaw({ source_uuid: call.uuid }, call.usage, call.model),
935
971
  }));
936
- const result = [primary, ...tokenEvents];
972
+ // Assistant dialogue content (text + thinking) for the same calls. Stored
973
+ // for dialogue/search; metric-neutral server-side (ASSISTANT_CONTENT_TYPES).
974
+ // data.* is truncated for display; raw.* keeps the full text (redacted
975
+ // server-side), mirroring how raw preserves full content elsewhere.
976
+ const contentEvents = [];
977
+ for (const call of calls) {
978
+ const base = {
979
+ event_id: null,
980
+ source: 'claude_code',
981
+ session_id: sessionId,
982
+ project_path: projectPath,
983
+ timestamp: call.timestamp || timestamp,
984
+ };
985
+ if (call.text) {
986
+ contentEvents.push({
987
+ ...base,
988
+ type: 'AssistantText',
989
+ data: { text: truncate(call.text, TRUNCATION_LIMITS.agentResponse), model: call.model },
990
+ raw: { source_uuid: call.uuid, model: call.model, text: call.text },
991
+ });
992
+ }
993
+ if (call.thinking) {
994
+ contentEvents.push({
995
+ ...base,
996
+ type: 'AssistantThinking',
997
+ data: { thinking: truncate(call.thinking, TRUNCATION_LIMITS.agentThought), model: call.model },
998
+ raw: { source_uuid: call.uuid, model: call.model, thinking: call.thinking },
999
+ });
1000
+ }
1001
+ }
1002
+ const result = [primary, ...tokenEvents, ...contentEvents];
937
1003
  // Attach the commit callback as a non-enumerable property on the returned
938
1004
  // array so it survives through normalizeEvent() without leaking into
939
1005
  // iteration (for...of, .map, writeToSpool's {...spread}) or into tests
@@ -1610,12 +1676,18 @@ async function main() {
1610
1676
  for (let i = 1; i < events.length; i++) {
1611
1677
  const ev = events[i];
1612
1678
  const sourceUuid = ev.raw && ev.raw.source_uuid;
1679
+ // Per-type kind keeps the three call-derived events (TokenUsage +
1680
+ // AssistantText + AssistantThinking) that share one assistant-line uuid
1681
+ // from colliding on a single id (which would dedup all but one away).
1682
+ const kind = ev.type === 'AssistantText' ? 'assistanttext'
1683
+ : ev.type === 'AssistantThinking' ? 'assistantthinking'
1684
+ : 'tokenusage';
1613
1685
  if (sourceUuid) {
1614
- ev.event_id = deterministicEventId(`claude_code:tokenusage:${sourceUuid}`);
1686
+ ev.event_id = deterministicEventId(`claude_code:${kind}:${sourceUuid}`);
1615
1687
  } else {
1616
1688
  // Fallback: stdin hash + per-event index. Should never be needed because
1617
1689
  // Claude Code transcripts always include uuid per record.
1618
- ev.event_id = deterministicEventId(`${input}:tokenusage:${i}`);
1690
+ ev.event_id = deterministicEventId(`${input}:${kind}:${i}`);
1619
1691
  }
1620
1692
  }
1621
1693
 
@@ -1640,7 +1712,10 @@ async function main() {
1640
1712
  // the next Stop re-reads the same delta (server-side dedup on event_id
1641
1713
  // handles the already-succeeded rows).
1642
1714
  if (ev === primary) process.exit(1);
1643
- if (ev.type === 'TokenUsage') allTokenUsageWritten = false;
1715
+ // Any transcript-derived event (TokenUsage / AssistantText /
1716
+ // AssistantThinking) failing means we must NOT advance the cursor, so the
1717
+ // next Stop re-reads the delta. Server-side ON CONFLICT dedups what landed.
1718
+ if (ev !== primary) allTokenUsageWritten = false;
1644
1719
  }
1645
1720
  }
1646
1721
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.112",
3
+ "version": "0.8.113",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {