@arcreflex/agent-transcripts 0.1.14 → 0.1.16
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/package.json +1 -1
- package/src/adapters/claude-code.ts +1 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/pi-coding-agent.ts +633 -0
- package/src/archive.ts +39 -0
- package/src/render.ts +6 -6
- package/src/serve.ts +206 -49
- package/src/types.ts +1 -0
- package/src/utils/summary.ts +3 -3
- package/test/adapters.test.ts +35 -1
- package/test/archive.test.ts +44 -1
- package/test/fixtures/claude/multiple-tools.output.md +3 -3
- package/test/fixtures/claude/skipped-message-chain.output.md +1 -1
- package/test/fixtures/claude/with-tools.output.md +1 -1
- package/test/fixtures/pi-coding-agent/basic-conversation.input.jsonl +5 -0
- package/test/fixtures/pi-coding-agent/basic-conversation.output.md +28 -0
- package/test/fixtures/pi-coding-agent/branching.input.jsonl +7 -0
- package/test/fixtures/pi-coding-agent/branching.output.md +25 -0
- package/test/fixtures/pi-coding-agent/with-compaction.input.jsonl +7 -0
- package/test/fixtures/pi-coding-agent/with-compaction.output.md +28 -0
- package/test/fixtures/pi-coding-agent/with-thinking.input.jsonl +3 -0
- package/test/fixtures/pi-coding-agent/with-thinking.output.md +21 -0
- package/test/fixtures/pi-coding-agent/with-tools.input.jsonl +5 -0
- package/test/fixtures/pi-coding-agent/with-tools.output.md +20 -0
- package/test/serve.test.ts +168 -0
- package/test/snapshots.test.ts +61 -37
- package/test/summary.test.ts +6 -0
package/package.json
CHANGED
package/src/adapters/index.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Adapter, SourceSpec } from "../types.ts";
|
|
6
6
|
import { claudeCodeAdapter } from "./claude-code.ts";
|
|
7
|
+
import { piCodingAgentAdapter } from "./pi-coding-agent.ts";
|
|
7
8
|
|
|
8
9
|
const adapters: Record<string, Adapter> = {
|
|
9
10
|
"claude-code": claudeCodeAdapter,
|
|
11
|
+
"pi-coding-agent": piCodingAgentAdapter,
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -15,6 +17,8 @@ const adapters: Record<string, Adapter> = {
|
|
|
15
17
|
const detectionRules: Array<{ pattern: RegExp; adapter: string }> = [
|
|
16
18
|
// Match .claude/ or /claude/ in path
|
|
17
19
|
{ pattern: /[./]claude[/\\]/, adapter: "claude-code" },
|
|
20
|
+
// Match .pi/agent/sessions/ in path
|
|
21
|
+
{ pattern: /[./]pi[/\\]agent[/\\]sessions[/\\]/, adapter: "pi-coding-agent" },
|
|
18
22
|
];
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-coding-agent JSONL adapter.
|
|
3
|
+
*
|
|
4
|
+
* Parses session files from ~/.pi/agent/sessions/{encoded-cwd}/{timestamp}_{uuid}.jsonl
|
|
5
|
+
*
|
|
6
|
+
* Session format: tree-structured JSONL with id/parentId linking (version 3).
|
|
7
|
+
* See: https://github.com/badlogic/pi-mono — pi-coding-agent session docs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Glob } from "bun";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { stat } from "fs/promises";
|
|
14
|
+
import type {
|
|
15
|
+
Adapter,
|
|
16
|
+
DiscoveredSession,
|
|
17
|
+
Transcript,
|
|
18
|
+
Message,
|
|
19
|
+
Warning,
|
|
20
|
+
ToolCall,
|
|
21
|
+
} from "../types.ts";
|
|
22
|
+
import { extractToolSummary } from "../utils/summary.ts";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Session file types (pi-coding-agent JSONL format, version 3)
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
interface SessionHeader {
|
|
29
|
+
type: "session";
|
|
30
|
+
version?: number;
|
|
31
|
+
id: string;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
parentSession?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SessionEntryBase {
|
|
38
|
+
type: string;
|
|
39
|
+
id: string;
|
|
40
|
+
parentId: string | null;
|
|
41
|
+
timestamp: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Content block types
|
|
45
|
+
|
|
46
|
+
interface TextContent {
|
|
47
|
+
type: "text";
|
|
48
|
+
text: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ImageContent {
|
|
52
|
+
type: "image";
|
|
53
|
+
data: string;
|
|
54
|
+
mimeType: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ThinkingContent {
|
|
58
|
+
type: "thinking";
|
|
59
|
+
thinking: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface PiToolCall {
|
|
63
|
+
type: "toolCall";
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
arguments: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ContentBlock = TextContent | ImageContent | ThinkingContent | PiToolCall;
|
|
70
|
+
|
|
71
|
+
// Message types (embedded in SessionMessageEntry)
|
|
72
|
+
|
|
73
|
+
interface UserMessage {
|
|
74
|
+
role: "user";
|
|
75
|
+
content: string | (TextContent | ImageContent)[];
|
|
76
|
+
timestamp: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface AssistantMessage {
|
|
80
|
+
role: "assistant";
|
|
81
|
+
content: (TextContent | ThinkingContent | PiToolCall)[];
|
|
82
|
+
api: string;
|
|
83
|
+
provider: string;
|
|
84
|
+
model: string;
|
|
85
|
+
stopReason: string;
|
|
86
|
+
errorMessage?: string;
|
|
87
|
+
timestamp: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ToolResultMessage {
|
|
91
|
+
role: "toolResult";
|
|
92
|
+
toolCallId: string;
|
|
93
|
+
toolName: string;
|
|
94
|
+
content: (TextContent | ImageContent)[];
|
|
95
|
+
isError: boolean;
|
|
96
|
+
timestamp: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface BashExecutionMessage {
|
|
100
|
+
role: "bashExecution";
|
|
101
|
+
command: string;
|
|
102
|
+
output: string;
|
|
103
|
+
exitCode: number | undefined;
|
|
104
|
+
cancelled: boolean;
|
|
105
|
+
truncated: boolean;
|
|
106
|
+
timestamp: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface CustomMessage {
|
|
110
|
+
role: "custom";
|
|
111
|
+
customType: string;
|
|
112
|
+
content: string | (TextContent | ImageContent)[];
|
|
113
|
+
display: boolean;
|
|
114
|
+
timestamp: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface BranchSummaryMessage {
|
|
118
|
+
role: "branchSummary";
|
|
119
|
+
summary: string;
|
|
120
|
+
fromId: string;
|
|
121
|
+
timestamp: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface CompactionSummaryMessage {
|
|
125
|
+
role: "compactionSummary";
|
|
126
|
+
summary: string;
|
|
127
|
+
tokensBefore: number;
|
|
128
|
+
timestamp: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type AgentMessage =
|
|
132
|
+
| UserMessage
|
|
133
|
+
| AssistantMessage
|
|
134
|
+
| ToolResultMessage
|
|
135
|
+
| BashExecutionMessage
|
|
136
|
+
| CustomMessage
|
|
137
|
+
| BranchSummaryMessage
|
|
138
|
+
| CompactionSummaryMessage;
|
|
139
|
+
|
|
140
|
+
// Entry types
|
|
141
|
+
|
|
142
|
+
interface SessionMessageEntry extends SessionEntryBase {
|
|
143
|
+
type: "message";
|
|
144
|
+
message: AgentMessage;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface CompactionEntry extends SessionEntryBase {
|
|
148
|
+
type: "compaction";
|
|
149
|
+
summary: string;
|
|
150
|
+
firstKeptEntryId: string;
|
|
151
|
+
tokensBefore: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface BranchSummaryEntry extends SessionEntryBase {
|
|
155
|
+
type: "branch_summary";
|
|
156
|
+
fromId: string;
|
|
157
|
+
summary: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface ModelChangeEntry extends SessionEntryBase {
|
|
161
|
+
type: "model_change";
|
|
162
|
+
provider: string;
|
|
163
|
+
modelId: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface ThinkingLevelChangeEntry extends SessionEntryBase {
|
|
167
|
+
type: "thinking_level_change";
|
|
168
|
+
thinkingLevel: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface SessionInfoEntry extends SessionEntryBase {
|
|
172
|
+
type: "session_info";
|
|
173
|
+
name?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Entries we care about for parsing
|
|
177
|
+
type SessionEntry =
|
|
178
|
+
| SessionMessageEntry
|
|
179
|
+
| CompactionEntry
|
|
180
|
+
| BranchSummaryEntry
|
|
181
|
+
| ModelChangeEntry
|
|
182
|
+
| ThinkingLevelChangeEntry
|
|
183
|
+
| SessionInfoEntry;
|
|
184
|
+
|
|
185
|
+
type FileEntry = SessionHeader | SessionEntry;
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Parsing helpers
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
function parseJsonl(content: string): {
|
|
192
|
+
header: SessionHeader | undefined;
|
|
193
|
+
entries: SessionEntry[];
|
|
194
|
+
warnings: Warning[];
|
|
195
|
+
} {
|
|
196
|
+
const entries: SessionEntry[] = [];
|
|
197
|
+
const warnings: Warning[] = [];
|
|
198
|
+
let header: SessionHeader | undefined;
|
|
199
|
+
const lines = content.split("\n");
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
const line = lines[i].trim();
|
|
203
|
+
if (!line) continue;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const record = JSON.parse(line) as FileEntry;
|
|
207
|
+
|
|
208
|
+
if (record.type === "session") {
|
|
209
|
+
header = record as SessionHeader;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Skip entry types we don't render (custom, label, custom_message, etc.)
|
|
214
|
+
if (
|
|
215
|
+
record.type === "message" ||
|
|
216
|
+
record.type === "compaction" ||
|
|
217
|
+
record.type === "branch_summary" ||
|
|
218
|
+
record.type === "model_change" ||
|
|
219
|
+
record.type === "thinking_level_change" ||
|
|
220
|
+
record.type === "session_info"
|
|
221
|
+
) {
|
|
222
|
+
entries.push(record as SessionEntry);
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
warnings.push({
|
|
226
|
+
type: "parse_error",
|
|
227
|
+
detail: `Line ${i + 1}: ${e instanceof Error ? e.message : "Invalid JSON"}`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { header, entries, warnings };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractText(content: string | ContentBlock[]): string {
|
|
236
|
+
if (typeof content === "string") return content;
|
|
237
|
+
return content
|
|
238
|
+
.flatMap((b) => {
|
|
239
|
+
if (b.type === "text") return [b.text];
|
|
240
|
+
return [];
|
|
241
|
+
})
|
|
242
|
+
.join("\n");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function extractThinking(content: ContentBlock[]): string | undefined {
|
|
246
|
+
const parts = content.flatMap((b) =>
|
|
247
|
+
b.type === "thinking" ? [b.thinking] : [],
|
|
248
|
+
);
|
|
249
|
+
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function extractPiToolCalls(content: ContentBlock[]): PiToolCall[] {
|
|
253
|
+
return content.filter((b): b is PiToolCall => b.type === "toolCall");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================================================
|
|
257
|
+
// Conversation splitting (tree structure via id/parentId)
|
|
258
|
+
// ============================================================================
|
|
259
|
+
|
|
260
|
+
interface SplitResult {
|
|
261
|
+
conversations: SessionEntry[][];
|
|
262
|
+
resolvedParents: Map<string, string | undefined>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function splitConversations(entries: SessionEntry[]): SplitResult {
|
|
266
|
+
if (entries.length === 0) {
|
|
267
|
+
return { conversations: [], resolvedParents: new Map() };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const byId = new Map<string, SessionEntry>();
|
|
271
|
+
const children = new Map<string, string[]>();
|
|
272
|
+
const resolvedParents = new Map<string, string | undefined>();
|
|
273
|
+
const roots: string[] = [];
|
|
274
|
+
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
byId.set(entry.id, entry);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
const parentId = entry.parentId;
|
|
281
|
+
if (parentId && byId.has(parentId)) {
|
|
282
|
+
resolvedParents.set(entry.id, parentId);
|
|
283
|
+
const existing = children.get(parentId) || [];
|
|
284
|
+
existing.push(entry.id);
|
|
285
|
+
children.set(parentId, existing);
|
|
286
|
+
} else {
|
|
287
|
+
resolvedParents.set(entry.id, undefined);
|
|
288
|
+
roots.push(entry.id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const visited = new Set<string>();
|
|
293
|
+
const conversations: SessionEntry[][] = [];
|
|
294
|
+
|
|
295
|
+
for (const root of roots) {
|
|
296
|
+
if (visited.has(root)) continue;
|
|
297
|
+
|
|
298
|
+
const conversation: SessionEntry[] = [];
|
|
299
|
+
const queue = [root];
|
|
300
|
+
|
|
301
|
+
while (queue.length > 0) {
|
|
302
|
+
const id = queue.shift();
|
|
303
|
+
if (!id || visited.has(id)) continue;
|
|
304
|
+
visited.add(id);
|
|
305
|
+
|
|
306
|
+
const entry = byId.get(id);
|
|
307
|
+
if (entry) conversation.push(entry);
|
|
308
|
+
|
|
309
|
+
const childIds = children.get(id) || [];
|
|
310
|
+
queue.push(...childIds);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (conversation.length > 0) {
|
|
314
|
+
conversations.push(conversation);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Sort conversations by first entry timestamp
|
|
319
|
+
conversations.sort((a, b) => {
|
|
320
|
+
const ta = new Date(a[0].timestamp).getTime();
|
|
321
|
+
const tb = new Date(b[0].timestamp).getTime();
|
|
322
|
+
return ta - tb;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return { conversations, resolvedParents };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// Transform entries → transcript messages
|
|
330
|
+
// ============================================================================
|
|
331
|
+
|
|
332
|
+
function transformConversation(
|
|
333
|
+
entries: SessionEntry[],
|
|
334
|
+
sourcePath: string,
|
|
335
|
+
warnings: Warning[],
|
|
336
|
+
resolvedParents: Map<string, string | undefined>,
|
|
337
|
+
cwd: string | undefined,
|
|
338
|
+
): Transcript {
|
|
339
|
+
const messages: Message[] = [];
|
|
340
|
+
|
|
341
|
+
// Collect tool results: toolCallId → { result, isError, toolName }
|
|
342
|
+
const toolResults = new Map<
|
|
343
|
+
string,
|
|
344
|
+
{ result: string; isError: boolean; toolName: string }
|
|
345
|
+
>();
|
|
346
|
+
|
|
347
|
+
// First pass: collect tool results from toolResult messages
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
if (entry.type !== "message") continue;
|
|
350
|
+
const msg = entry.message;
|
|
351
|
+
if (msg.role === "toolResult") {
|
|
352
|
+
const text = extractText(msg.content);
|
|
353
|
+
toolResults.set(msg.toolCallId, {
|
|
354
|
+
result: text,
|
|
355
|
+
isError: msg.isError,
|
|
356
|
+
toolName: msg.toolName,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Track which entry IDs produce messages (for parent resolution through skipped entries)
|
|
362
|
+
const skippedParents = new Map<string, string | undefined>();
|
|
363
|
+
|
|
364
|
+
// Identify entries that will be skipped
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
let willSkip = false;
|
|
367
|
+
|
|
368
|
+
if (entry.type === "message") {
|
|
369
|
+
const msg = entry.message;
|
|
370
|
+
if (msg.role === "toolResult" || msg.role === "bashExecution") {
|
|
371
|
+
willSkip = true;
|
|
372
|
+
} else if (msg.role === "user") {
|
|
373
|
+
const text = extractText(msg.content);
|
|
374
|
+
if (!text.trim()) willSkip = true;
|
|
375
|
+
} else if (msg.role === "assistant") {
|
|
376
|
+
const text = extractText(msg.content);
|
|
377
|
+
const thinking = extractThinking(msg.content as ContentBlock[]);
|
|
378
|
+
const toolCalls = extractPiToolCalls(msg.content as ContentBlock[]);
|
|
379
|
+
if (!text.trim() && !thinking && toolCalls.length === 0) {
|
|
380
|
+
willSkip = true;
|
|
381
|
+
}
|
|
382
|
+
} else if (
|
|
383
|
+
msg.role === "compactionSummary" ||
|
|
384
|
+
msg.role === "branchSummary" ||
|
|
385
|
+
msg.role === "custom"
|
|
386
|
+
) {
|
|
387
|
+
willSkip = true;
|
|
388
|
+
}
|
|
389
|
+
} else if (
|
|
390
|
+
entry.type === "model_change" ||
|
|
391
|
+
entry.type === "thinking_level_change" ||
|
|
392
|
+
entry.type === "session_info"
|
|
393
|
+
) {
|
|
394
|
+
willSkip = true;
|
|
395
|
+
}
|
|
396
|
+
// compaction and branch_summary entries → rendered as system messages, not skipped
|
|
397
|
+
|
|
398
|
+
if (willSkip) {
|
|
399
|
+
skippedParents.set(entry.id, resolvedParents.get(entry.id));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Resolve parent through skipped entries
|
|
404
|
+
function resolveParent(entryId: string): string | undefined {
|
|
405
|
+
let current = resolvedParents.get(entryId);
|
|
406
|
+
const visited = new Set<string>();
|
|
407
|
+
while (current && skippedParents.has(current)) {
|
|
408
|
+
if (visited.has(current)) return undefined;
|
|
409
|
+
visited.add(current);
|
|
410
|
+
current = skippedParents.get(current);
|
|
411
|
+
}
|
|
412
|
+
return current;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Second pass: build messages
|
|
416
|
+
for (const entry of entries) {
|
|
417
|
+
if (skippedParents.has(entry.id)) continue;
|
|
418
|
+
|
|
419
|
+
const sourceRef = entry.id;
|
|
420
|
+
const timestamp = entry.timestamp;
|
|
421
|
+
const parentMessageRef = resolveParent(entry.id);
|
|
422
|
+
const rawJson = JSON.stringify(entry);
|
|
423
|
+
|
|
424
|
+
if (entry.type === "compaction") {
|
|
425
|
+
messages.push({
|
|
426
|
+
type: "system",
|
|
427
|
+
sourceRef,
|
|
428
|
+
timestamp,
|
|
429
|
+
parentMessageRef,
|
|
430
|
+
rawJson,
|
|
431
|
+
content: `[Compaction] ${entry.summary}`,
|
|
432
|
+
});
|
|
433
|
+
} else if (entry.type === "branch_summary") {
|
|
434
|
+
messages.push({
|
|
435
|
+
type: "system",
|
|
436
|
+
sourceRef,
|
|
437
|
+
timestamp,
|
|
438
|
+
parentMessageRef,
|
|
439
|
+
rawJson,
|
|
440
|
+
content: `[Branch summary] ${entry.summary}`,
|
|
441
|
+
});
|
|
442
|
+
} else if (entry.type === "message") {
|
|
443
|
+
const msg = entry.message;
|
|
444
|
+
|
|
445
|
+
if (msg.role === "user") {
|
|
446
|
+
const text = extractText(msg.content);
|
|
447
|
+
if (text.trim()) {
|
|
448
|
+
messages.push({
|
|
449
|
+
type: "user",
|
|
450
|
+
sourceRef,
|
|
451
|
+
timestamp,
|
|
452
|
+
parentMessageRef,
|
|
453
|
+
rawJson,
|
|
454
|
+
content: text,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
} else if (msg.role === "assistant") {
|
|
458
|
+
const blocks = msg.content as ContentBlock[];
|
|
459
|
+
const text = extractText(blocks);
|
|
460
|
+
const thinking = extractThinking(blocks);
|
|
461
|
+
const piToolCalls = extractPiToolCalls(blocks);
|
|
462
|
+
|
|
463
|
+
if (text.trim() || thinking) {
|
|
464
|
+
messages.push({
|
|
465
|
+
type: "assistant",
|
|
466
|
+
sourceRef,
|
|
467
|
+
timestamp,
|
|
468
|
+
parentMessageRef,
|
|
469
|
+
rawJson,
|
|
470
|
+
content: text,
|
|
471
|
+
thinking,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (piToolCalls.length > 0) {
|
|
476
|
+
const calls: ToolCall[] = piToolCalls.map((tc) => {
|
|
477
|
+
const result = toolResults.get(tc.id);
|
|
478
|
+
return {
|
|
479
|
+
id: tc.id,
|
|
480
|
+
name: tc.name,
|
|
481
|
+
summary: extractToolSummary(tc.name, tc.arguments || {}),
|
|
482
|
+
input: tc.arguments,
|
|
483
|
+
result: result?.result,
|
|
484
|
+
error: result?.isError ? result.result : undefined,
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
messages.push({
|
|
488
|
+
type: "tool_calls",
|
|
489
|
+
sourceRef,
|
|
490
|
+
timestamp,
|
|
491
|
+
parentMessageRef,
|
|
492
|
+
rawJson,
|
|
493
|
+
calls,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Compute time bounds
|
|
501
|
+
let minTime = Infinity;
|
|
502
|
+
let maxTime = -Infinity;
|
|
503
|
+
for (const msg of messages) {
|
|
504
|
+
const t = new Date(msg.timestamp).getTime();
|
|
505
|
+
if (t < minTime) minTime = t;
|
|
506
|
+
if (t > maxTime) maxTime = t;
|
|
507
|
+
}
|
|
508
|
+
const now = new Date().toISOString();
|
|
509
|
+
const startTime = Number.isFinite(minTime)
|
|
510
|
+
? new Date(minTime).toISOString()
|
|
511
|
+
: now;
|
|
512
|
+
const endTime = Number.isFinite(maxTime)
|
|
513
|
+
? new Date(maxTime).toISOString()
|
|
514
|
+
: startTime;
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
source: {
|
|
518
|
+
file: sourcePath,
|
|
519
|
+
adapter: "pi-coding-agent",
|
|
520
|
+
},
|
|
521
|
+
metadata: {
|
|
522
|
+
warnings,
|
|
523
|
+
messageCount: messages.length,
|
|
524
|
+
startTime,
|
|
525
|
+
endTime,
|
|
526
|
+
cwd,
|
|
527
|
+
},
|
|
528
|
+
messages,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// Adapter
|
|
534
|
+
// ============================================================================
|
|
535
|
+
|
|
536
|
+
export const piCodingAgentAdapter: Adapter = {
|
|
537
|
+
name: "pi-coding-agent",
|
|
538
|
+
version: "pi-coding-agent:1",
|
|
539
|
+
defaultSource: join(homedir(), ".pi", "agent", "sessions"),
|
|
540
|
+
|
|
541
|
+
async discover(source: string): Promise<DiscoveredSession[]> {
|
|
542
|
+
const sessions: DiscoveredSession[] = [];
|
|
543
|
+
const glob = new Glob("**/*.jsonl");
|
|
544
|
+
|
|
545
|
+
for await (const file of glob.scan({ cwd: source, absolute: false })) {
|
|
546
|
+
const fullPath = join(source, file);
|
|
547
|
+
try {
|
|
548
|
+
const fileStat = await stat(fullPath);
|
|
549
|
+
sessions.push({
|
|
550
|
+
path: fullPath,
|
|
551
|
+
relativePath: file,
|
|
552
|
+
mtime: fileStat.mtime.getTime(),
|
|
553
|
+
});
|
|
554
|
+
} catch {
|
|
555
|
+
// Skip files we can't stat
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Try to extract session name from session_info entries
|
|
560
|
+
for (const session of sessions) {
|
|
561
|
+
try {
|
|
562
|
+
const content = await Bun.file(session.path).text();
|
|
563
|
+
const lines = content.split("\n");
|
|
564
|
+
// Scan for session_info entries (last one wins)
|
|
565
|
+
let name: string | undefined;
|
|
566
|
+
for (const line of lines) {
|
|
567
|
+
if (!line.trim()) continue;
|
|
568
|
+
try {
|
|
569
|
+
const entry = JSON.parse(line);
|
|
570
|
+
if (entry.type === "session_info" && entry.name) {
|
|
571
|
+
name = entry.name;
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
// skip
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (name) {
|
|
578
|
+
session.summary = name;
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
// skip
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return sessions;
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
parse(content: string, sourcePath: string): Transcript[] {
|
|
589
|
+
const { header, entries, warnings } = parseJsonl(content);
|
|
590
|
+
const cwd = header?.cwd;
|
|
591
|
+
|
|
592
|
+
const { conversations, resolvedParents } = splitConversations(entries);
|
|
593
|
+
|
|
594
|
+
if (conversations.length === 0) {
|
|
595
|
+
const now = new Date().toISOString();
|
|
596
|
+
return [
|
|
597
|
+
{
|
|
598
|
+
source: { file: sourcePath, adapter: "pi-coding-agent" },
|
|
599
|
+
metadata: {
|
|
600
|
+
warnings,
|
|
601
|
+
messageCount: 0,
|
|
602
|
+
startTime: now,
|
|
603
|
+
endTime: now,
|
|
604
|
+
cwd,
|
|
605
|
+
},
|
|
606
|
+
messages: [],
|
|
607
|
+
},
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (conversations.length === 1) {
|
|
612
|
+
return [
|
|
613
|
+
transformConversation(
|
|
614
|
+
conversations[0],
|
|
615
|
+
sourcePath,
|
|
616
|
+
warnings,
|
|
617
|
+
resolvedParents,
|
|
618
|
+
cwd,
|
|
619
|
+
),
|
|
620
|
+
];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return conversations.map((conv, i) =>
|
|
624
|
+
transformConversation(
|
|
625
|
+
conv,
|
|
626
|
+
sourcePath,
|
|
627
|
+
i === 0 ? warnings : [],
|
|
628
|
+
resolvedParents,
|
|
629
|
+
cwd,
|
|
630
|
+
),
|
|
631
|
+
);
|
|
632
|
+
},
|
|
633
|
+
};
|