@arcreflex/agent-transcripts 0.1.8 → 0.1.10

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.
@@ -17,46 +17,281 @@ import type {
17
17
  } from "../types.ts";
18
18
  import { extractToolSummary } from "../utils/summary.ts";
19
19
 
20
+ // ============================================================================
21
+ // sessions-index.json types
22
+ // ============================================================================
23
+
20
24
  /**
21
- * Claude Code sessions-index.json structure.
25
+ * Root structure of sessions-index.json file.
26
+ * Contains metadata about all Claude Code sessions for a project.
22
27
  */
23
28
  interface SessionsIndex {
29
+ /** Schema version for the index file format */
24
30
  version: number;
31
+ /** Array of session metadata entries */
25
32
  entries: SessionIndexEntry[];
33
+ /**
34
+ * Original project path (workspace path).
35
+ * This is the actual project directory being worked on.
36
+ */
37
+ originalPath: string;
26
38
  }
27
39
 
40
+ /**
41
+ * Metadata for a single Claude Code session.
42
+ * Each entry corresponds to one .jsonl session file.
43
+ */
28
44
  interface SessionIndexEntry {
45
+ /** Unique session identifier (UUID format) */
29
46
  sessionId: string;
47
+ /**
48
+ * Absolute path to the session .jsonl file.
49
+ * Typically in ~/.claude/projects/<project-name>/<sessionId>.jsonl
50
+ */
30
51
  fullPath: string;
52
+ /**
53
+ * File modification time in Unix epoch milliseconds.
54
+ * Used to detect file changes and for sorting.
55
+ */
31
56
  fileMtime: number;
57
+ /**
58
+ * First user prompt in the session (may be truncated with "…").
59
+ * Will be "No prompt" for empty sessions or "[Request interrupted by user]"
60
+ * for sessions that were interrupted before completion.
61
+ */
62
+ firstPrompt: string;
63
+ /**
64
+ * Human-readable summary of the session.
65
+ * Generated by Claude Code, describes what the session accomplished.
66
+ * Common patterns:
67
+ * - "Conversation Cleared" / "Empty Conversation - Clear Command Only" for cleared sessions
68
+ * - Action-oriented descriptions for active sessions
69
+ */
70
+ summary: string;
71
+ /** Total number of messages in the session */
72
+ messageCount: number;
73
+ /** ISO 8601 timestamp when the session was created */
74
+ created: string;
75
+ /** ISO 8601 timestamp when the session was last modified */
76
+ modified: string;
77
+ /**
78
+ * Git branch that was active when the session was created/active.
79
+ * Useful for correlating sessions with code changes.
80
+ */
81
+ gitBranch: string;
82
+ /**
83
+ * Project workspace path.
84
+ * This is the working directory for the session.
85
+ */
86
+ projectPath: string;
87
+ /**
88
+ * Whether this session is a "sidechain" (subagent) session.
89
+ * Subagent sessions are spawned by parent sessions for specialized tasks.
90
+ * Main sessions have isSidechain: false.
91
+ */
92
+ isSidechain: boolean;
93
+ }
94
+
95
+ // ============================================================================
96
+ // JSONL record types
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Top-level JSONL record type discriminated by `type` field.
101
+ * Each line in a Claude Code session .jsonl file is one of these record types.
102
+ */
103
+ type ClaudeRecord =
104
+ | FileHistorySnapshot
105
+ | UserRecord
106
+ | AssistantRecord
107
+ | SystemRecord
108
+ | ProgressRecord
109
+ | SummaryRecord;
110
+
111
+ /**
112
+ * Records that participate in the message tree (have uuid, parentUuid, timestamp).
113
+ * These are the record types that the parsing code processes for transcript generation.
114
+ */
115
+ type MessageRecord =
116
+ | UserRecord
117
+ | AssistantRecord
118
+ | SystemRecord
119
+ | ProgressRecord;
120
+
121
+ /** Type guard for message records */
122
+ function isMessageRecord(rec: ClaudeRecord): rec is MessageRecord {
123
+ return (
124
+ rec.type === "user" ||
125
+ rec.type === "assistant" ||
126
+ rec.type === "system" ||
127
+ rec.type === "progress"
128
+ );
129
+ }
130
+
131
+ /** Common fields shared by most record types */
132
+ interface BaseRecord {
133
+ uuid: string;
134
+ parentUuid: string | null;
135
+ timestamp: string;
32
136
  isSidechain: boolean;
137
+ userType: "external";
138
+ cwd: string;
139
+ sessionId: string;
140
+ version: string;
141
+ gitBranch: string;
142
+ slug?: string;
143
+ }
144
+
145
+ interface FileHistorySnapshot {
146
+ type: "file-history-snapshot";
147
+ messageId: string;
148
+ snapshot: {
149
+ messageId: string;
150
+ trackedFileBackups: Record<
151
+ string,
152
+ {
153
+ backupFileName: string;
154
+ version: number;
155
+ backupTime: string;
156
+ }
157
+ >;
158
+ timestamp: string;
159
+ };
160
+ isSnapshotUpdate: boolean;
33
161
  }
34
162
 
35
- // Claude Code JSONL record types
36
- interface ClaudeRecord {
37
- type: string;
38
- uuid?: string;
39
- parentUuid?: string | null;
40
- timestamp?: string;
41
- message?: {
42
- role: string;
163
+ interface UserRecord extends BaseRecord {
164
+ type: "user";
165
+ message: {
166
+ role: "user";
43
167
  content: string | ContentBlock[];
44
168
  };
45
- content?: string;
46
- subtype?: string;
169
+ /** Present when message is metadata (e.g., command caveats) */
170
+ isMeta?: boolean;
171
+ /** Present when message is a tool result */
172
+ toolUseResult?: {
173
+ task?: {
174
+ id: string;
175
+ subject: string;
176
+ };
177
+ [key: string]: unknown;
178
+ };
179
+ /** UUID of the assistant message that triggered this tool use */
180
+ sourceToolAssistantUUID?: string;
181
+ }
182
+
183
+ interface AssistantRecord extends BaseRecord {
184
+ type: "assistant";
185
+ message: {
186
+ model: string;
187
+ id: string;
188
+ type: "message";
189
+ role: "assistant";
190
+ content: ContentBlock[];
191
+ stop_reason: string | null;
192
+ stop_sequence: string | null;
193
+ usage: {
194
+ input_tokens: number;
195
+ cache_creation_input_tokens?: number;
196
+ cache_read_input_tokens?: number;
197
+ cache_creation?: {
198
+ ephemeral_5m_input_tokens: number;
199
+ ephemeral_1h_input_tokens: number;
200
+ };
201
+ output_tokens: number;
202
+ service_tier?: string;
203
+ };
204
+ };
205
+ requestId: string;
47
206
  }
48
207
 
49
- interface ContentBlock {
50
- type: string;
51
- text?: string;
52
- thinking?: string;
53
- id?: string;
54
- name?: string;
55
- input?: Record<string, unknown>;
56
- tool_use_id?: string;
208
+ interface SystemRecord extends BaseRecord {
209
+ type: "system";
210
+ /** Subtypes: "turn_duration", etc. */
211
+ subtype: string;
212
+ isMeta: boolean;
213
+ /** Present for turn_duration subtype */
214
+ durationMs?: number;
215
+ /** Text content (if any) - some system messages may include display text */
57
216
  content?: string;
58
217
  }
59
218
 
219
+ interface ProgressRecord extends BaseRecord {
220
+ type: "progress";
221
+ data: AgentProgressData | HookProgressData;
222
+ /** Tool use ID that triggered this progress */
223
+ toolUseID?: string;
224
+ /** Parent tool use ID (for nested tool calls) */
225
+ parentToolUseID?: string;
226
+ }
227
+
228
+ interface AgentProgressData {
229
+ type: "agent_progress";
230
+ message: {
231
+ type: "user";
232
+ message: {
233
+ role: "user";
234
+ content: ContentBlock[];
235
+ };
236
+ uuid: string;
237
+ timestamp: string;
238
+ };
239
+ normalizedMessages: unknown[];
240
+ /** The prompt text sent to the agent */
241
+ prompt: string;
242
+ /** Agent identifier */
243
+ agentId: string;
244
+ }
245
+
246
+ interface HookProgressData {
247
+ type: "hook_progress";
248
+ hookEvent: string;
249
+ hookName: string;
250
+ command: string;
251
+ }
252
+
253
+ interface SummaryRecord {
254
+ type: "summary";
255
+ /** Human-readable summary text */
256
+ summary: string;
257
+ /** UUID of the leaf message in the conversation tree */
258
+ leafUuid: string;
259
+ }
260
+
261
+ // ============================================================================
262
+ // Content block types
263
+ // ============================================================================
264
+
265
+ /**
266
+ * Content blocks appear in message.content arrays for both user and assistant messages.
267
+ */
268
+ type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock;
269
+
270
+ interface TextBlock {
271
+ type: "text";
272
+ text: string;
273
+ }
274
+
275
+ interface ThinkingBlock {
276
+ type: "thinking";
277
+ thinking: string;
278
+ }
279
+
280
+ interface ToolUseBlock {
281
+ type: "tool_use";
282
+ id: string;
283
+ name: string;
284
+ input: Record<string, unknown>;
285
+ }
286
+
287
+ interface ToolResultBlock {
288
+ type: "tool_result";
289
+ tool_use_id: string;
290
+ content: string | ContentBlock[];
291
+ /** Present when tool result indicates an error */
292
+ is_error?: boolean;
293
+ }
294
+
60
295
  /**
61
296
  * Parse JSONL content with best-effort error recovery.
62
297
  */
@@ -86,52 +321,109 @@ function parseJsonl(content: string): {
86
321
  return { records, warnings };
87
322
  }
88
323
 
324
+ /** Any record that can participate in parent-child relationships */
325
+ type TraversableRecord = { uuid: string; parentUuid: string | null };
326
+
327
+ /**
328
+ * Find the nearest message ancestor by walking up the parent chain.
329
+ * Returns undefined if no message ancestor exists.
330
+ */
331
+ function findMessageAncestor(
332
+ parentUuid: string | null | undefined,
333
+ allByUuid: Map<string, TraversableRecord>,
334
+ messageUuids: Set<string>,
335
+ ): string | undefined {
336
+ const visited = new Set<string>();
337
+ let current = parentUuid;
338
+ while (current) {
339
+ if (visited.has(current)) {
340
+ return undefined; // Cycle detected
341
+ }
342
+ visited.add(current);
343
+ if (messageUuids.has(current)) {
344
+ return current;
345
+ }
346
+ const rec = allByUuid.get(current);
347
+ current = rec?.parentUuid ?? null;
348
+ }
349
+ return undefined;
350
+ }
351
+
352
+ /** Message record types that appear in conversation trees */
353
+ type ConversationRecord = UserRecord | AssistantRecord | SystemRecord;
354
+
355
+ /** Result of splitting records into conversation trees */
356
+ interface SplitResult {
357
+ conversations: ConversationRecord[][];
358
+ /** Map from message UUID to its resolved parent (nearest message ancestor) */
359
+ resolvedParents: Map<string, string | undefined>;
360
+ }
361
+
89
362
  /**
90
363
  * Build message graph and find conversation boundaries.
91
- * Returns array of conversation groups (each is array of records in order).
364
+ * Returns conversations and a map of resolved parent references.
92
365
  */
93
- function splitConversations(records: ClaudeRecord[]): ClaudeRecord[][] {
94
- // Filter to only message records (user, assistant, system with uuid)
366
+ function splitConversations(records: ClaudeRecord[]): SplitResult {
367
+ // Filter to only message records (user, assistant, system - records with uuid)
95
368
  const messageRecords = records.filter(
96
- (r) =>
97
- r.uuid &&
98
- (r.type === "user" || r.type === "assistant" || r.type === "system"),
369
+ (r): r is UserRecord | AssistantRecord | SystemRecord =>
370
+ r.type === "user" || r.type === "assistant" || r.type === "system",
99
371
  );
100
372
 
101
- if (messageRecords.length === 0) return [];
102
-
103
- // Build parent → children map
104
- const byUuid = new Map<string, ClaudeRecord>();
105
- const children = new Map<string, string[]>();
373
+ if (messageRecords.length === 0) {
374
+ return { conversations: [], resolvedParents: new Map() };
375
+ }
106
376
 
107
- for (const rec of messageRecords) {
108
- if (rec.uuid) {
109
- byUuid.set(rec.uuid, rec);
110
- const parent = rec.parentUuid;
111
- if (parent) {
112
- const existing = children.get(parent) || [];
113
- existing.push(rec.uuid);
114
- children.set(parent, existing);
115
- }
377
+ // Build UUID lookup for ALL records with uuid/parentUuid to enable
378
+ // traversing through non-message records (e.g., progress) when resolving parents
379
+ const allByUuid = new Map<string, TraversableRecord>();
380
+ for (const rec of records) {
381
+ if (isMessageRecord(rec)) {
382
+ allByUuid.set(rec.uuid, rec);
116
383
  }
117
384
  }
118
385
 
119
- // Find roots (no parent or parent not in our set)
386
+ // Set of message UUIDs for quick lookup
387
+ const messageUuids = new Set<string>(messageRecords.map((r) => r.uuid));
388
+
389
+ // Build parent → children map, resolving through non-message records
390
+ // Also track resolved parents for use in transformation
391
+ const byUuid = new Map<string, ConversationRecord>();
392
+ const children = new Map<string, string[]>();
393
+ const resolvedParents = new Map<string, string | undefined>();
120
394
  const roots: string[] = [];
395
+
121
396
  for (const rec of messageRecords) {
122
- if (!rec.parentUuid || !byUuid.has(rec.parentUuid)) {
123
- if (rec.uuid) roots.push(rec.uuid);
397
+ byUuid.set(rec.uuid, rec);
398
+
399
+ // Find nearest message ancestor (walking through non-message records)
400
+ const ancestor = findMessageAncestor(
401
+ rec.parentUuid,
402
+ allByUuid,
403
+ messageUuids,
404
+ );
405
+
406
+ // Store resolved parent for this message
407
+ resolvedParents.set(rec.uuid, ancestor);
408
+
409
+ if (ancestor) {
410
+ const existing = children.get(ancestor) || [];
411
+ existing.push(rec.uuid);
412
+ children.set(ancestor, existing);
413
+ } else {
414
+ // No message ancestor - this is a root
415
+ roots.push(rec.uuid);
124
416
  }
125
417
  }
126
418
 
127
419
  // BFS from each root to collect conversation
128
420
  const visited = new Set<string>();
129
- const conversations: ClaudeRecord[][] = [];
421
+ const conversations: ConversationRecord[][] = [];
130
422
 
131
423
  for (const root of roots) {
132
424
  if (visited.has(root)) continue;
133
425
 
134
- const conversation: ClaudeRecord[] = [];
426
+ const conversation: ConversationRecord[] = [];
135
427
  const queue = [root];
136
428
 
137
429
  while (queue.length > 0) {
@@ -155,12 +447,12 @@ function splitConversations(records: ClaudeRecord[]): ClaudeRecord[][] {
155
447
 
156
448
  // Sort conversations by their first message timestamp
157
449
  conversations.sort((a, b) => {
158
- const ta = a[0]?.timestamp ? new Date(a[0].timestamp).getTime() : 0;
159
- const tb = b[0]?.timestamp ? new Date(b[0].timestamp).getTime() : 0;
450
+ const ta = new Date(a[0].timestamp).getTime();
451
+ const tb = new Date(b[0].timestamp).getTime();
160
452
  return ta - tb;
161
453
  });
162
454
 
163
- return conversations;
455
+ return { conversations, resolvedParents };
164
456
  }
165
457
 
166
458
  /**
@@ -189,16 +481,23 @@ function extractThinking(content: string | ContentBlock[]): string | undefined {
189
481
 
190
482
  /**
191
483
  * Extract tool calls from content blocks.
484
+ * Matches with results from the toolResults map.
192
485
  */
193
- function extractToolCalls(content: string | ContentBlock[]): ToolCall[] {
486
+ function extractToolCalls(
487
+ content: string | ContentBlock[],
488
+ toolResults: Map<string, string>,
489
+ ): ToolCall[] {
194
490
  if (typeof content === "string") return [];
195
491
 
196
492
  return content.flatMap((b) => {
197
- if (b.type === "tool_use" && b.name) {
493
+ if (b.type === "tool_use" && b.name && b.id) {
494
+ const result = toolResults.get(b.id);
198
495
  return [
199
496
  {
200
497
  name: b.name,
201
498
  summary: extractToolSummary(b.name, b.input || {}),
499
+ input: b.input,
500
+ result,
202
501
  },
203
502
  ];
204
503
  }
@@ -206,6 +505,39 @@ function extractToolCalls(content: string | ContentBlock[]): ToolCall[] {
206
505
  });
207
506
  }
208
507
 
508
+ /**
509
+ * Safely convert tool result content to string.
510
+ * Content can be a string, array, or other structure.
511
+ */
512
+ function stringifyToolResult(content: unknown): string {
513
+ if (typeof content === "string") return content;
514
+ if (content === null || content === undefined) return "";
515
+ // For arrays or objects, JSON stringify for display
516
+ try {
517
+ return JSON.stringify(content, null, 2);
518
+ } catch {
519
+ return String(content);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Extract tool results from content blocks.
525
+ * Returns a map of tool_use_id → result content.
526
+ */
527
+ function extractToolResults(
528
+ content: string | ContentBlock[],
529
+ ): Map<string, string> {
530
+ const results = new Map<string, string>();
531
+ if (typeof content === "string") return results;
532
+
533
+ for (const b of content) {
534
+ if (b.type === "tool_result" && b.tool_use_id && b.content !== undefined) {
535
+ results.set(b.tool_use_id, stringifyToolResult(b.content));
536
+ }
537
+ }
538
+ return results;
539
+ }
540
+
209
541
  /**
210
542
  * Check if a user message contains only tool results (no actual user text).
211
543
  */
@@ -230,8 +562,13 @@ function resolveParent(
230
562
  if (!parentUuid) return undefined;
231
563
 
232
564
  // Follow the chain through any skipped messages
565
+ const visited = new Set<string>();
233
566
  let current: string | undefined = parentUuid;
234
567
  while (current && skippedParents.has(current)) {
568
+ if (visited.has(current)) {
569
+ return undefined; // Cycle detected
570
+ }
571
+ visited.add(current);
235
572
  current = skippedParents.get(current);
236
573
  }
237
574
 
@@ -242,31 +579,48 @@ function resolveParent(
242
579
  * Transform a conversation into our intermediate format.
243
580
  */
244
581
  function transformConversation(
245
- records: ClaudeRecord[],
582
+ records: ConversationRecord[],
246
583
  sourcePath: string,
247
584
  warnings: Warning[],
585
+ resolvedParents: Map<string, string | undefined>,
248
586
  ): Transcript {
249
587
  const messages: Message[] = [];
250
- // Track skipped message UUIDs → their parent UUIDs for chain repair
588
+ // Track skipped message UUIDs → their resolved parent UUIDs for chain repair
251
589
  const skippedParents = new Map<string, string | undefined>();
252
590
 
253
- // First pass: identify which messages will be skipped
591
+ // Collect all tool results from user messages (tool_use_id result)
592
+ const allToolResults = new Map<string, string>();
254
593
  for (const rec of records) {
255
- if (!rec.uuid) continue;
594
+ if (rec.type === "user") {
595
+ const results = extractToolResults(rec.message.content);
596
+ for (const [id, content] of results) {
597
+ allToolResults.set(id, content);
598
+ }
599
+ }
600
+ }
256
601
 
602
+ let cwd: string | undefined;
603
+
604
+ // First pass: identify which messages will be skipped
605
+ for (const rec of records) {
257
606
  let willSkip = false;
258
607
 
259
- if (rec.type === "user" && rec.message) {
608
+ // Take the first cwd we find.
609
+ if (!cwd) {
610
+ cwd = rec.cwd;
611
+ }
612
+
613
+ if (rec.type === "user") {
260
614
  if (isToolResultOnly(rec.message.content)) {
261
615
  willSkip = true;
262
616
  } else {
263
617
  const text = extractText(rec.message.content);
264
618
  if (!text.trim()) willSkip = true;
265
619
  }
266
- } else if (rec.type === "assistant" && rec.message) {
620
+ } else if (rec.type === "assistant") {
267
621
  const text = extractText(rec.message.content);
268
622
  const thinking = extractThinking(rec.message.content);
269
- const toolCalls = extractToolCalls(rec.message.content);
623
+ const toolCalls = extractToolCalls(rec.message.content, allToolResults);
270
624
  // Only skip if no text, no thinking, AND no tool calls
271
625
  if (!text.trim() && !thinking && toolCalls.length === 0) {
272
626
  willSkip = true;
@@ -277,17 +631,24 @@ function transformConversation(
277
631
  }
278
632
 
279
633
  if (willSkip) {
280
- skippedParents.set(rec.uuid, rec.parentUuid || undefined);
634
+ // Use the resolved parent (already walked through non-message records)
635
+ skippedParents.set(rec.uuid, resolvedParents.get(rec.uuid));
281
636
  }
282
637
  }
283
638
 
284
639
  // Second pass: build messages with corrected parent references
285
640
  for (const rec of records) {
286
- const sourceRef = rec.uuid || "";
287
- const timestamp = rec.timestamp || new Date().toISOString();
288
- const parentMessageRef = resolveParent(rec.parentUuid, skippedParents);
641
+ const sourceRef = rec.uuid;
642
+ const timestamp = rec.timestamp;
643
+ // Start with the resolved parent (through non-message records),
644
+ // then walk through any skipped messages
645
+ const parentMessageRef = resolveParent(
646
+ resolvedParents.get(rec.uuid),
647
+ skippedParents,
648
+ );
649
+ const rawJson = JSON.stringify(rec);
289
650
 
290
- if (rec.type === "user" && rec.message) {
651
+ if (rec.type === "user") {
291
652
  // Skip tool-result-only user messages (they're just tool responses)
292
653
  if (isToolResultOnly(rec.message.content)) continue;
293
654
 
@@ -298,13 +659,14 @@ function transformConversation(
298
659
  sourceRef,
299
660
  timestamp,
300
661
  parentMessageRef,
662
+ rawJson,
301
663
  content: text,
302
664
  });
303
665
  }
304
- } else if (rec.type === "assistant" && rec.message) {
666
+ } else if (rec.type === "assistant") {
305
667
  const text = extractText(rec.message.content);
306
668
  const thinking = extractThinking(rec.message.content);
307
- const toolCalls = extractToolCalls(rec.message.content);
669
+ const toolCalls = extractToolCalls(rec.message.content, allToolResults);
308
670
 
309
671
  // Add assistant message if there's text or thinking
310
672
  if (text.trim() || thinking) {
@@ -313,6 +675,7 @@ function transformConversation(
313
675
  sourceRef,
314
676
  timestamp,
315
677
  parentMessageRef,
678
+ rawJson,
316
679
  content: text,
317
680
  thinking,
318
681
  });
@@ -325,6 +688,7 @@ function transformConversation(
325
688
  sourceRef,
326
689
  timestamp,
327
690
  parentMessageRef,
691
+ rawJson,
328
692
  calls: toolCalls,
329
693
  });
330
694
  }
@@ -336,18 +700,42 @@ function transformConversation(
336
700
  sourceRef,
337
701
  timestamp,
338
702
  parentMessageRef,
703
+ rawJson,
339
704
  content: text,
340
705
  });
341
706
  }
342
707
  }
343
708
  }
344
709
 
710
+ // Compute time bounds from min/max across all messages (not array order,
711
+ // which is BFS traversal order and may not be chronological for branches)
712
+ let minTime = Infinity;
713
+ let maxTime = -Infinity;
714
+ for (const msg of messages) {
715
+ const t = new Date(msg.timestamp).getTime();
716
+ if (t < minTime) minTime = t;
717
+ if (t > maxTime) maxTime = t;
718
+ }
719
+ const now = new Date().toISOString();
720
+ const startTime = Number.isFinite(minTime)
721
+ ? new Date(minTime).toISOString()
722
+ : now;
723
+ const endTime = Number.isFinite(maxTime)
724
+ ? new Date(maxTime).toISOString()
725
+ : startTime;
726
+
345
727
  return {
346
728
  source: {
347
729
  file: sourcePath,
348
730
  adapter: "claude-code",
349
731
  },
350
- metadata: { warnings },
732
+ metadata: {
733
+ warnings,
734
+ messageCount: messages.length,
735
+ startTime,
736
+ endTime,
737
+ cwd,
738
+ },
351
739
  messages,
352
740
  };
353
741
  }
@@ -383,6 +771,7 @@ async function discoverFromIndex(
383
771
  relativePath:
384
772
  relative(source, entry.fullPath) || basename(entry.fullPath),
385
773
  mtime: fileStat.mtime.getTime(),
774
+ summary: entry.summary,
386
775
  });
387
776
  } catch {
388
777
  // Skip files that no longer exist
@@ -435,14 +824,21 @@ export const claudeCodeAdapter: Adapter = {
435
824
 
436
825
  parse(content: string, sourcePath: string): Transcript[] {
437
826
  const { records, warnings } = parseJsonl(content);
438
- const conversations = splitConversations(records);
827
+ const { conversations, resolvedParents } = splitConversations(records);
439
828
 
440
829
  if (conversations.length === 0) {
441
830
  // Return single empty transcript with warnings
831
+ const now = new Date().toISOString();
442
832
  return [
443
833
  {
444
834
  source: { file: sourcePath, adapter: "claude-code" },
445
- metadata: { warnings },
835
+ metadata: {
836
+ warnings,
837
+ messageCount: 0,
838
+ startTime: now,
839
+ endTime: now,
840
+ cwd: undefined,
841
+ },
446
842
  messages: [],
447
843
  },
448
844
  ];
@@ -450,12 +846,24 @@ export const claudeCodeAdapter: Adapter = {
450
846
 
451
847
  // For single conversation, include all warnings
452
848
  if (conversations.length === 1) {
453
- return [transformConversation(conversations[0], sourcePath, warnings)];
849
+ return [
850
+ transformConversation(
851
+ conversations[0],
852
+ sourcePath,
853
+ warnings,
854
+ resolvedParents,
855
+ ),
856
+ ];
454
857
  }
455
858
 
456
859
  // For multiple conversations, only first gets warnings
457
860
  return conversations.map((conv, i) =>
458
- transformConversation(conv, sourcePath, i === 0 ? warnings : []),
861
+ transformConversation(
862
+ conv,
863
+ sourcePath,
864
+ i === 0 ? warnings : [],
865
+ resolvedParents,
866
+ ),
459
867
  );
460
868
  },
461
869
  };