@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.
- package/CLAUDE.md +4 -0
- package/README.md +52 -3
- package/bun.lock +89 -0
- package/package.json +3 -2
- package/scripts/infer-cc-types.prose +87 -0
- package/src/adapters/claude-code.ts +476 -68
- package/src/cache.ts +129 -0
- package/src/cli.ts +86 -5
- package/src/convert.ts +6 -14
- package/src/render-html.ts +1096 -0
- package/src/render-index.ts +611 -0
- package/src/render.ts +6 -110
- package/src/serve.ts +308 -0
- package/src/sync.ts +132 -18
- package/src/title.ts +172 -0
- package/src/types.ts +9 -0
- package/src/utils/html.ts +12 -0
- package/src/utils/openrouter.ts +116 -0
- package/src/utils/provenance.ts +25 -41
- package/src/utils/tree.ts +116 -0
- package/test/fixtures/claude/non-message-parents.input.jsonl +9 -0
- package/test/fixtures/claude/non-message-parents.output.md +30 -0
|
@@ -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
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
type:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
364
|
+
* Returns conversations and a map of resolved parent references.
|
|
92
365
|
*/
|
|
93
|
-
function splitConversations(records: 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.
|
|
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)
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
123
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
159
|
-
const tb =
|
|
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(
|
|
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:
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
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"
|
|
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"
|
|
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: {
|
|
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: {
|
|
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 [
|
|
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(
|
|
861
|
+
transformConversation(
|
|
862
|
+
conv,
|
|
863
|
+
sourcePath,
|
|
864
|
+
i === 0 ? warnings : [],
|
|
865
|
+
resolvedParents,
|
|
866
|
+
),
|
|
459
867
|
);
|
|
460
868
|
},
|
|
461
869
|
};
|