@arcreflex/agent-transcripts 0.1.9 → 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.
@@ -17,45 +17,279 @@ 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
  };
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;
206
+ }
207
+
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 */
45
216
  content?: string;
46
- subtype?: string;
47
- cwd?: string;
48
217
  }
49
218
 
50
- interface ContentBlock {
51
- type: string;
52
- text?: string;
53
- thinking?: string;
54
- id?: string;
55
- name?: string;
56
- input?: Record<string, unknown>;
57
- tool_use_id?: string;
58
- content?: unknown; // Can be string, array, or other structure
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;
59
293
  }
60
294
 
61
295
  /**
@@ -87,13 +321,16 @@ function parseJsonl(content: string): {
87
321
  return { records, warnings };
88
322
  }
89
323
 
324
+ /** Any record that can participate in parent-child relationships */
325
+ type TraversableRecord = { uuid: string; parentUuid: string | null };
326
+
90
327
  /**
91
328
  * Find the nearest message ancestor by walking up the parent chain.
92
329
  * Returns undefined if no message ancestor exists.
93
330
  */
94
331
  function findMessageAncestor(
95
332
  parentUuid: string | null | undefined,
96
- allByUuid: Map<string, ClaudeRecord>,
333
+ allByUuid: Map<string, TraversableRecord>,
97
334
  messageUuids: Set<string>,
98
335
  ): string | undefined {
99
336
  const visited = new Set<string>();
@@ -112,8 +349,12 @@ function findMessageAncestor(
112
349
  return undefined;
113
350
  }
114
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 */
115
356
  interface SplitResult {
116
- conversations: ClaudeRecord[][];
357
+ conversations: ConversationRecord[][];
117
358
  /** Map from message UUID to its resolved parent (nearest message ancestor) */
118
359
  resolvedParents: Map<string, string | undefined>;
119
360
  }
@@ -123,40 +364,36 @@ interface SplitResult {
123
364
  * Returns conversations and a map of resolved parent references.
124
365
  */
125
366
  function splitConversations(records: ClaudeRecord[]): SplitResult {
126
- // Filter to only message records (user, assistant, system with uuid)
367
+ // Filter to only message records (user, assistant, system - records with uuid)
127
368
  const messageRecords = records.filter(
128
- (r) =>
129
- r.uuid &&
130
- (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",
131
371
  );
132
372
 
133
373
  if (messageRecords.length === 0) {
134
374
  return { conversations: [], resolvedParents: new Map() };
135
375
  }
136
376
 
137
- // Build UUID lookup for ALL records to track parent chains through non-messages
138
- const allByUuid = new Map<string, ClaudeRecord>();
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>();
139
380
  for (const rec of records) {
140
- if (rec.uuid) {
381
+ if (isMessageRecord(rec)) {
141
382
  allByUuid.set(rec.uuid, rec);
142
383
  }
143
384
  }
144
385
 
145
386
  // Set of message UUIDs for quick lookup
146
- const messageUuids = new Set<string>();
147
- for (const rec of messageRecords) {
148
- if (rec.uuid) messageUuids.add(rec.uuid);
149
- }
387
+ const messageUuids = new Set<string>(messageRecords.map((r) => r.uuid));
150
388
 
151
389
  // Build parent → children map, resolving through non-message records
152
390
  // Also track resolved parents for use in transformation
153
- const byUuid = new Map<string, ClaudeRecord>();
391
+ const byUuid = new Map<string, ConversationRecord>();
154
392
  const children = new Map<string, string[]>();
155
393
  const resolvedParents = new Map<string, string | undefined>();
156
394
  const roots: string[] = [];
157
395
 
158
396
  for (const rec of messageRecords) {
159
- if (!rec.uuid) continue;
160
397
  byUuid.set(rec.uuid, rec);
161
398
 
162
399
  // Find nearest message ancestor (walking through non-message records)
@@ -181,12 +418,12 @@ function splitConversations(records: ClaudeRecord[]): SplitResult {
181
418
 
182
419
  // BFS from each root to collect conversation
183
420
  const visited = new Set<string>();
184
- const conversations: ClaudeRecord[][] = [];
421
+ const conversations: ConversationRecord[][] = [];
185
422
 
186
423
  for (const root of roots) {
187
424
  if (visited.has(root)) continue;
188
425
 
189
- const conversation: ClaudeRecord[] = [];
426
+ const conversation: ConversationRecord[] = [];
190
427
  const queue = [root];
191
428
 
192
429
  while (queue.length > 0) {
@@ -210,8 +447,8 @@ function splitConversations(records: ClaudeRecord[]): SplitResult {
210
447
 
211
448
  // Sort conversations by their first message timestamp
212
449
  conversations.sort((a, b) => {
213
- const ta = a[0]?.timestamp ? new Date(a[0].timestamp).getTime() : 0;
214
- 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();
215
452
  return ta - tb;
216
453
  });
217
454
 
@@ -342,7 +579,7 @@ function resolveParent(
342
579
  * Transform a conversation into our intermediate format.
343
580
  */
344
581
  function transformConversation(
345
- records: ClaudeRecord[],
582
+ records: ConversationRecord[],
346
583
  sourcePath: string,
347
584
  warnings: Warning[],
348
585
  resolvedParents: Map<string, string | undefined>,
@@ -354,7 +591,7 @@ function transformConversation(
354
591
  // Collect all tool results from user messages (tool_use_id → result)
355
592
  const allToolResults = new Map<string, string>();
356
593
  for (const rec of records) {
357
- if (rec.type === "user" && rec.message) {
594
+ if (rec.type === "user") {
358
595
  const results = extractToolResults(rec.message.content);
359
596
  for (const [id, content] of results) {
360
597
  allToolResults.set(id, content);
@@ -366,23 +603,21 @@ function transformConversation(
366
603
 
367
604
  // First pass: identify which messages will be skipped
368
605
  for (const rec of records) {
369
- if (!rec.uuid) continue;
370
-
371
606
  let willSkip = false;
372
607
 
373
608
  // Take the first cwd we find.
374
- if (!cwd && rec.cwd) {
609
+ if (!cwd) {
375
610
  cwd = rec.cwd;
376
611
  }
377
612
 
378
- if (rec.type === "user" && rec.message) {
613
+ if (rec.type === "user") {
379
614
  if (isToolResultOnly(rec.message.content)) {
380
615
  willSkip = true;
381
616
  } else {
382
617
  const text = extractText(rec.message.content);
383
618
  if (!text.trim()) willSkip = true;
384
619
  }
385
- } else if (rec.type === "assistant" && rec.message) {
620
+ } else if (rec.type === "assistant") {
386
621
  const text = extractText(rec.message.content);
387
622
  const thinking = extractThinking(rec.message.content);
388
623
  const toolCalls = extractToolCalls(rec.message.content, allToolResults);
@@ -403,16 +638,17 @@ function transformConversation(
403
638
 
404
639
  // Second pass: build messages with corrected parent references
405
640
  for (const rec of records) {
406
- const sourceRef = rec.uuid || "";
407
- const timestamp = rec.timestamp || new Date().toISOString();
641
+ const sourceRef = rec.uuid;
642
+ const timestamp = rec.timestamp;
408
643
  // Start with the resolved parent (through non-message records),
409
644
  // then walk through any skipped messages
410
- const parentMessageRef = rec.uuid
411
- ? resolveParent(resolvedParents.get(rec.uuid), skippedParents)
412
- : undefined;
645
+ const parentMessageRef = resolveParent(
646
+ resolvedParents.get(rec.uuid),
647
+ skippedParents,
648
+ );
413
649
  const rawJson = JSON.stringify(rec);
414
650
 
415
- if (rec.type === "user" && rec.message) {
651
+ if (rec.type === "user") {
416
652
  // Skip tool-result-only user messages (they're just tool responses)
417
653
  if (isToolResultOnly(rec.message.content)) continue;
418
654
 
@@ -427,7 +663,7 @@ function transformConversation(
427
663
  content: text,
428
664
  });
429
665
  }
430
- } else if (rec.type === "assistant" && rec.message) {
666
+ } else if (rec.type === "assistant") {
431
667
  const text = extractText(rec.message.content);
432
668
  const thinking = extractThinking(rec.message.content);
433
669
  const toolCalls = extractToolCalls(rec.message.content, allToolResults);
@@ -535,6 +771,7 @@ async function discoverFromIndex(
535
771
  relativePath:
536
772
  relative(source, entry.fullPath) || basename(entry.fullPath),
537
773
  mtime: fileStat.mtime.getTime(),
774
+ summary: entry.summary,
538
775
  });
539
776
  } catch {
540
777
  // Skip files that no longer exist
@@ -578,6 +815,7 @@ async function discoverByGlob(source: string): Promise<DiscoveredSession[]> {
578
815
 
579
816
  export const claudeCodeAdapter: Adapter = {
580
817
  name: "claude-code",
818
+ version: "claude-code:1",
581
819
 
582
820
  async discover(source: string): Promise<DiscoveredSession[]> {
583
821
  // Try index-based discovery first, fall back to glob
@@ -30,16 +30,10 @@ export function detectAdapter(filePath: string): string | undefined {
30
30
  return undefined;
31
31
  }
32
32
 
33
- /**
34
- * Get adapter by name.
35
- */
36
33
  export function getAdapter(name: string): Adapter | undefined {
37
34
  return adapters[name];
38
35
  }
39
36
 
40
- /**
41
- * List available adapter names.
42
- */
43
37
  export function listAdapters(): string[] {
44
38
  return Object.keys(adapters);
45
39
  }