spiracha 1.0.0
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/AGENTS.md +128 -0
- package/LICENSE.md +7 -0
- package/README.md +182 -0
- package/package.json +70 -0
- package/src/export-chats.ts +134 -0
- package/src/export-claude.ts +36 -0
- package/src/lib/claude-exporter.ts +864 -0
- package/src/lib/codex-exporter-cli.ts +272 -0
- package/src/lib/codex-exporter-db.ts +320 -0
- package/src/lib/codex-exporter-transcript.ts +684 -0
- package/src/lib/codex-exporter-types.ts +110 -0
- package/src/lib/codex-exporter.ts +116 -0
- package/src/lib/interactive-cli.ts +448 -0
- package/src/lib/shared.ts +224 -0
- package/src/mcp-server.ts +136 -0
- package/src/spiracha.ts +72 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { matchesFilters, toCodexRelativePath } from './codex-exporter-db';
|
|
3
|
+
import type { CodexCliOptions, ExportTarget, MessageRecord, SessionMeta, ToolRecord } from './codex-exporter-types';
|
|
4
|
+
import {
|
|
5
|
+
asObject,
|
|
6
|
+
asString,
|
|
7
|
+
cleanExtractedText,
|
|
8
|
+
cleanInlineTitle,
|
|
9
|
+
type ExportFormat,
|
|
10
|
+
formatInlineLiteral,
|
|
11
|
+
type JsonValue,
|
|
12
|
+
type MetadataEntry,
|
|
13
|
+
readJsonlObjects,
|
|
14
|
+
renderDocumentTitle,
|
|
15
|
+
renderMetadataBlock,
|
|
16
|
+
renderSection,
|
|
17
|
+
} from './shared';
|
|
18
|
+
|
|
19
|
+
export const convertSessionFile = async (target: ExportTarget, options: CodexCliOptions): Promise<string | null> => {
|
|
20
|
+
let transcriptState: CodexTranscriptState;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
transcriptState = await collectCodexTranscript(target.sessionFile, options);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
throw new Error(`Failed to read Codex transcript ${target.sessionFile}: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!matchesFilters(target.thread?.cwd ?? transcriptState.sessionMeta.cwd ?? null, options)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (transcriptState.sections.length === 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options.optimized) {
|
|
38
|
+
return transcriptState.sections.join('\n\n').trimEnd() + '\n';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const title = getTitle(target, transcriptState.sessionMeta);
|
|
42
|
+
const metadata = buildMetadataEntries(target, transcriptState.sessionMeta, options);
|
|
43
|
+
const parts = [
|
|
44
|
+
renderDocumentTitle(title, options.outputFormat),
|
|
45
|
+
'',
|
|
46
|
+
renderMetadataBlock(metadata, options.outputFormat),
|
|
47
|
+
...transcriptState.sections,
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
return parts.join('\n').trimEnd() + '\n';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type CodexTranscriptState = {
|
|
53
|
+
sessionMeta: SessionMeta;
|
|
54
|
+
sections: string[];
|
|
55
|
+
startedTranscript: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const collectCodexTranscript = async (sessionFile: string, options: CodexCliOptions): Promise<CodexTranscriptState> => {
|
|
59
|
+
const state: CodexTranscriptState = {
|
|
60
|
+
sections: [],
|
|
61
|
+
sessionMeta: {},
|
|
62
|
+
startedTranscript: false,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for await (const parsed of readJsonlObjects(sessionFile)) {
|
|
66
|
+
processCodexTranscriptRecord(parsed, options, state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return state;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const processCodexTranscriptRecord = (
|
|
73
|
+
parsed: Record<string, JsonValue>,
|
|
74
|
+
options: CodexCliOptions,
|
|
75
|
+
state: CodexTranscriptState,
|
|
76
|
+
) => {
|
|
77
|
+
captureSessionMeta(parsed, state.sessionMeta);
|
|
78
|
+
|
|
79
|
+
const message = extractMessageRecord(parsed);
|
|
80
|
+
if (message) {
|
|
81
|
+
processCodexMessageRecord(message, options, state);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!options.includeTools) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tool = extractToolRecord(parsed);
|
|
90
|
+
if (!tool) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const block = options.optimized
|
|
95
|
+
? renderCompactToolBlock(tool, options.outputFormat)
|
|
96
|
+
: renderToolBlock(tool, options.outputFormat);
|
|
97
|
+
if (block) {
|
|
98
|
+
state.sections.push(block);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const processCodexMessageRecord = (message: MessageRecord, options: CodexCliOptions, state: CodexTranscriptState) => {
|
|
103
|
+
if (options.optimized) {
|
|
104
|
+
processOptimizedCodexMessageRecord(message, options, state);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const block = renderMessageBlock(message, options.outputFormat);
|
|
109
|
+
if (block) {
|
|
110
|
+
state.sections.push(block);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const processOptimizedCodexMessageRecord = (
|
|
115
|
+
message: MessageRecord,
|
|
116
|
+
options: CodexCliOptions,
|
|
117
|
+
state: CodexTranscriptState,
|
|
118
|
+
) => {
|
|
119
|
+
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const compact = compactMessageText(message, true);
|
|
124
|
+
if (!compact) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!state.startedTranscript) {
|
|
129
|
+
if (shouldSkipOptimizedPrelude(message.role, compact)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
state.startedTranscript = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const rendered = renderCompactBlock(message, compact, options.outputFormat);
|
|
136
|
+
if (rendered) {
|
|
137
|
+
state.sections.push(rendered);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const compactMessageText = (message: MessageRecord, optimized: boolean): string => {
|
|
142
|
+
const rawText = extractText(message.content);
|
|
143
|
+
const cleaned = stripPreviewBlock(rawText);
|
|
144
|
+
|
|
145
|
+
return optimized ? optimizePlainText(optimizeRenderedText(cleaned)) : cleaned.trim();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const formatToolOutputSummary = (outputText: string, outputFormat: ExportFormat): string => {
|
|
149
|
+
if (!outputText) {
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lines = outputText
|
|
154
|
+
.split('\n')
|
|
155
|
+
.map((line) => line.trim())
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
|
|
158
|
+
if (lines.length === 0) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const summaryLines: string[] = [];
|
|
163
|
+
const command = lines.find((line) => line.startsWith('Command: '));
|
|
164
|
+
const exit = lines.find((line) => line.startsWith('Process exited with code '));
|
|
165
|
+
const wall = lines.find((line) => line.startsWith('Wall time: '));
|
|
166
|
+
|
|
167
|
+
if (command) {
|
|
168
|
+
summaryLines.push(command);
|
|
169
|
+
}
|
|
170
|
+
if (exit) {
|
|
171
|
+
summaryLines.push(exit);
|
|
172
|
+
}
|
|
173
|
+
if (wall) {
|
|
174
|
+
summaryLines.push(wall);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (outputFormat === 'md') {
|
|
178
|
+
return summaryLines.map((line) => `*${line}*`).join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return summaryLines.join('\n');
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const parseExecCommandArguments = (argumentsText?: string) => {
|
|
185
|
+
if (!argumentsText) {
|
|
186
|
+
return { cmd: null as string | null, workdir: null as string | null };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(argumentsText) as Record<string, unknown>;
|
|
191
|
+
return {
|
|
192
|
+
cmd: typeof parsed.cmd === 'string' ? parsed.cmd : null,
|
|
193
|
+
workdir: typeof parsed.workdir === 'string' ? parsed.workdir : null,
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
return { cmd: null as string | null, workdir: null as string | null };
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const getTitle = (target: ExportTarget, sessionMeta: SessionMeta): string => {
|
|
201
|
+
if (target.thread?.title) {
|
|
202
|
+
return cleanInlineTitle(target.thread.title);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return sessionMeta.id ?? path.basename(target.sessionFile, '.jsonl');
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const shouldSkipOptimizedPrelude = (role: string, text: string): boolean => {
|
|
209
|
+
if (role !== 'user') {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
text.startsWith('AGENTS.md instructions for ') ||
|
|
215
|
+
text.startsWith('# AGENTS.md instructions for ') ||
|
|
216
|
+
text.startsWith('<permissions instructions>') ||
|
|
217
|
+
text.startsWith('<environment_context>') ||
|
|
218
|
+
text.startsWith('<app-context>') ||
|
|
219
|
+
text.startsWith('<collaboration_mode>') ||
|
|
220
|
+
text.startsWith('<skills_instructions>') ||
|
|
221
|
+
text.startsWith('You are Codex, a coding agent based on GPT-5.') ||
|
|
222
|
+
text.startsWith('Read this before making changes.') ||
|
|
223
|
+
text.includes('Filesystem sandboxing defines which files can be read or written.') ||
|
|
224
|
+
text.includes('approval_policy') ||
|
|
225
|
+
text.includes('base_instructions')
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const buildMetadataEntries = (
|
|
230
|
+
target: ExportTarget,
|
|
231
|
+
sessionMeta: SessionMeta,
|
|
232
|
+
options: CodexCliOptions,
|
|
233
|
+
): MetadataEntry[] => {
|
|
234
|
+
return [
|
|
235
|
+
...buildCodexExportIdentityMetadata(target, sessionMeta),
|
|
236
|
+
...buildCodexExportPathMetadata(target, options),
|
|
237
|
+
...buildCodexRelationMetadata(target),
|
|
238
|
+
...buildCodexThreadMetadata(target, sessionMeta),
|
|
239
|
+
...buildCodexAgentMetadata(target),
|
|
240
|
+
];
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const buildCodexExportIdentityMetadata = (target: ExportTarget, sessionMeta: SessionMeta): MetadataEntry[] => {
|
|
244
|
+
const thread = target.thread;
|
|
245
|
+
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
key: 'exported_from',
|
|
249
|
+
value: thread ? 'thread_db_and_session_jsonl' : 'session_jsonl_fallback',
|
|
250
|
+
},
|
|
251
|
+
{ key: 'fallback_reason', value: target.fallbackReason },
|
|
252
|
+
{ key: 'thread_id', value: thread?.id ?? sessionMeta.id ?? null },
|
|
253
|
+
{ key: 'title', value: thread?.title || null },
|
|
254
|
+
];
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const buildCodexExportPathMetadata = (target: ExportTarget, options: CodexCliOptions): MetadataEntry[] => {
|
|
258
|
+
const relativeOutputPath = target.outputRelativePath;
|
|
259
|
+
|
|
260
|
+
return [
|
|
261
|
+
{ key: 'source_output_relative_path', value: relativeOutputPath },
|
|
262
|
+
{
|
|
263
|
+
key: options.outputFormat === 'md' ? 'source_markdown_path' : 'source_text_path',
|
|
264
|
+
value: relativeOutputPath,
|
|
265
|
+
},
|
|
266
|
+
{ key: 'rollout_path', value: target.sessionFile },
|
|
267
|
+
{
|
|
268
|
+
key: 'rollout_path_relative_to_codex',
|
|
269
|
+
value: toCodexRelativePath(target.sessionFile),
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const buildCodexRelationMetadata = (target: ExportTarget): MetadataEntry[] => {
|
|
275
|
+
const childThreadIds = target.relations.childEdges.map((edge) => edge.child_thread_id);
|
|
276
|
+
const childEdges = target.relations.childEdges.map((edge) => ({
|
|
277
|
+
child_thread_id: edge.child_thread_id,
|
|
278
|
+
status: edge.status,
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
return [
|
|
282
|
+
{ key: 'parent_thread_id', value: target.relations.parentThreadId },
|
|
283
|
+
{ key: 'child_thread_ids', value: childThreadIds },
|
|
284
|
+
{ key: 'spawn_edges', value: childEdges },
|
|
285
|
+
];
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const buildCodexThreadMetadata = (target: ExportTarget, sessionMeta: SessionMeta): MetadataEntry[] => {
|
|
289
|
+
return [
|
|
290
|
+
...buildCodexThreadTimingMetadata(target, sessionMeta),
|
|
291
|
+
...buildCodexThreadIdentityMetadata(target, sessionMeta),
|
|
292
|
+
];
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const buildCodexThreadTimingMetadata = (target: ExportTarget, sessionMeta: SessionMeta): MetadataEntry[] => {
|
|
296
|
+
const thread = target.thread;
|
|
297
|
+
|
|
298
|
+
return [
|
|
299
|
+
{ key: 'created_at_unix', value: thread?.created_at ?? null },
|
|
300
|
+
{ key: 'created_at_iso', value: formatUnixSeconds(thread?.created_at ?? null) },
|
|
301
|
+
{ key: 'updated_at_unix', value: thread?.updated_at ?? null },
|
|
302
|
+
{ key: 'updated_at_iso', value: formatUnixSeconds(thread?.updated_at ?? null) },
|
|
303
|
+
{ key: 'archived_at_unix', value: thread?.archived_at ?? null },
|
|
304
|
+
{ key: 'archived_at_iso', value: formatUnixSeconds(thread?.archived_at ?? null) },
|
|
305
|
+
{ key: 'session_started_at_iso', value: sessionMeta.timestamp ?? null },
|
|
306
|
+
];
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const buildCodexThreadIdentityMetadata = (target: ExportTarget, sessionMeta: SessionMeta): MetadataEntry[] => {
|
|
310
|
+
const thread = target.thread;
|
|
311
|
+
|
|
312
|
+
return [
|
|
313
|
+
{ key: 'archived', value: thread ? Boolean(thread.archived) : null },
|
|
314
|
+
{ key: 'source', value: thread?.source ?? sessionMeta.source ?? null },
|
|
315
|
+
{ key: 'originator', value: sessionMeta.originator ?? null },
|
|
316
|
+
{ key: 'model_provider', value: thread?.model_provider ?? null },
|
|
317
|
+
{ key: 'model', value: thread?.model ?? null },
|
|
318
|
+
{ key: 'reasoning_effort', value: thread?.reasoning_effort ?? null },
|
|
319
|
+
{
|
|
320
|
+
key: 'cli_version',
|
|
321
|
+
value: thread?.cli_version || sessionMeta.cli_version || null,
|
|
322
|
+
},
|
|
323
|
+
{ key: 'cwd', value: thread?.cwd || sessionMeta.cwd || null },
|
|
324
|
+
{ key: 'approval_mode', value: thread?.approval_mode ?? null },
|
|
325
|
+
{
|
|
326
|
+
key: 'sandbox_policy',
|
|
327
|
+
value: parseJsonSafely(thread?.sandbox_policy ?? null),
|
|
328
|
+
},
|
|
329
|
+
{ key: 'memory_mode', value: thread?.memory_mode ?? null },
|
|
330
|
+
{ key: 'tokens_used', value: thread?.tokens_used ?? null },
|
|
331
|
+
{ key: 'has_user_event', value: thread ? Boolean(thread.has_user_event) : null },
|
|
332
|
+
];
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const buildCodexAgentMetadata = (target: ExportTarget): MetadataEntry[] => {
|
|
336
|
+
const thread = target.thread;
|
|
337
|
+
|
|
338
|
+
return [
|
|
339
|
+
{ key: 'git_sha', value: thread?.git_sha ?? null },
|
|
340
|
+
{ key: 'git_branch', value: thread?.git_branch ?? null },
|
|
341
|
+
{ key: 'git_origin_url', value: thread?.git_origin_url ?? null },
|
|
342
|
+
{ key: 'agent_nickname', value: thread?.agent_nickname ?? null },
|
|
343
|
+
{ key: 'agent_role', value: thread?.agent_role ?? null },
|
|
344
|
+
{ key: 'agent_path', value: thread?.agent_path ?? null },
|
|
345
|
+
{ key: 'first_user_message', value: thread?.first_user_message || null },
|
|
346
|
+
];
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const parseJsonSafely = (value: string | null): unknown => {
|
|
350
|
+
if (!value) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
return JSON.parse(value) as unknown;
|
|
356
|
+
} catch {
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const formatUnixSeconds = (value: number | null): string | null => {
|
|
362
|
+
if (value === null || value === undefined) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return new Date(value * 1000).toISOString();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const captureSessionMeta = (parsed: Record<string, JsonValue>, meta: SessionMeta) => {
|
|
370
|
+
if (parsed.type !== 'session_meta') {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const payload = asObject(parsed.payload);
|
|
375
|
+
if (!payload) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
meta.id = asString(payload.id) ?? meta.id;
|
|
380
|
+
meta.timestamp = asString(payload.timestamp) ?? meta.timestamp;
|
|
381
|
+
meta.cwd = asString(payload.cwd) ?? meta.cwd;
|
|
382
|
+
meta.source = asString(payload.source) ?? meta.source;
|
|
383
|
+
meta.originator = asString(payload.originator) ?? meta.originator;
|
|
384
|
+
meta.cli_version = asString(payload.cli_version) ?? meta.cli_version;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const extractMessageRecord = (parsed: Record<string, JsonValue>): MessageRecord | null => {
|
|
388
|
+
if (parsed.type === 'message') {
|
|
389
|
+
const directMessage = normalizeMessage(parsed);
|
|
390
|
+
if (directMessage) {
|
|
391
|
+
return directMessage;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (parsed.type !== 'response_item') {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const payload = asObject(parsed.payload);
|
|
400
|
+
if (!payload || payload.type !== 'message') {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return normalizeMessage(payload);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const normalizeMessage = (value: Record<string, JsonValue>): MessageRecord | null => {
|
|
408
|
+
const role = asString(value.role);
|
|
409
|
+
const content = value.content;
|
|
410
|
+
const phase = asString(value.phase);
|
|
411
|
+
|
|
412
|
+
if (!role || content === undefined) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { content, phase: phase ?? undefined, role };
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null => {
|
|
420
|
+
if (parsed.type !== 'response_item') {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const payload = asObject(parsed.payload);
|
|
425
|
+
if (!payload) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (payload.type === 'function_call') {
|
|
430
|
+
const name = asString(payload.name);
|
|
431
|
+
const argumentsText = asString(payload.arguments);
|
|
432
|
+
const callId = asString(payload.call_id);
|
|
433
|
+
|
|
434
|
+
if (name !== 'exec_command') {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
argumentsText: argumentsText ?? undefined,
|
|
440
|
+
callId,
|
|
441
|
+
kind: 'call',
|
|
442
|
+
name,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (payload.type === 'function_call_output') {
|
|
447
|
+
const callId = asString(payload.call_id);
|
|
448
|
+
const outputText = asString(payload.output);
|
|
449
|
+
|
|
450
|
+
if (!outputText?.includes('Command: ')) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
callId,
|
|
456
|
+
kind: 'output',
|
|
457
|
+
name: 'function_call_output',
|
|
458
|
+
outputText: outputText ?? undefined,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return null;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const renderMessageBlock = (message: MessageRecord, outputFormat: ExportFormat): string => {
|
|
466
|
+
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
467
|
+
return '';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const text = cleanExtractedText(extractText(message.content)).trim();
|
|
471
|
+
if (!text || shouldSkipMessage(message.role, text)) {
|
|
472
|
+
return '';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const title = message.role === 'user' ? 'User' : 'Assistant';
|
|
476
|
+
const body = message.phase ? `Phase: ${message.phase}\n\n${text}` : text;
|
|
477
|
+
|
|
478
|
+
return renderSection(title, body, outputFormat);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const renderToolBlock = (tool: ToolRecord, outputFormat: ExportFormat): string => {
|
|
482
|
+
if (tool.kind === 'call') {
|
|
483
|
+
const details = formatToolCallDetails(tool, outputFormat);
|
|
484
|
+
return details ? renderSection('Tool', details, outputFormat) : '';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const summary = formatToolOutputSummary(tool.outputText ?? '', outputFormat);
|
|
488
|
+
return summary ? renderSection('Tool Output', summary, outputFormat) : '';
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const renderCompactBlock = (message: MessageRecord, text: string, outputFormat: ExportFormat): string => {
|
|
492
|
+
const prefix = message.role === 'user' ? 'U:' : 'A:';
|
|
493
|
+
const lines = text.split('\n');
|
|
494
|
+
const [firstLine, ...rest] = lines;
|
|
495
|
+
|
|
496
|
+
if (rest.length === 0) {
|
|
497
|
+
return `${prefix} ${normalizeCompactLiteral(firstLine, outputFormat)}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return [
|
|
501
|
+
`${prefix} ${normalizeCompactLiteral(firstLine, outputFormat)}`,
|
|
502
|
+
...rest.map((line) => normalizeCompactLiteral(line, outputFormat)),
|
|
503
|
+
].join('\n');
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const renderCompactToolBlock = (tool: ToolRecord, outputFormat: ExportFormat): string => {
|
|
507
|
+
if (tool.kind === 'call') {
|
|
508
|
+
const details = formatCompactToolCall(tool, outputFormat);
|
|
509
|
+
return details ? `T: ${details}` : '';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const summary = formatCompactToolOutput(tool.outputText ?? '');
|
|
513
|
+
return summary ? `R: ${summary}` : '';
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const stripPreviewBlock = (text: string): string => {
|
|
517
|
+
const parts = text
|
|
518
|
+
.split(/\n{2,}/)
|
|
519
|
+
.map((part) => part.trim())
|
|
520
|
+
.filter(Boolean);
|
|
521
|
+
|
|
522
|
+
if (parts.length < 2) {
|
|
523
|
+
return text.trim();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const first = parts[0];
|
|
527
|
+
const second = parts[1];
|
|
528
|
+
const looksLikePreview =
|
|
529
|
+
!/^([UA]):/i.test(first) &&
|
|
530
|
+
!/^##\s+(User|Assistant)\s*$/i.test(first) &&
|
|
531
|
+
/^([UA]):/i.test(second) === false &&
|
|
532
|
+
/^##\s+(User|Assistant)\s*$/i.test(second);
|
|
533
|
+
|
|
534
|
+
if (!looksLikePreview) {
|
|
535
|
+
return text.trim();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return parts.slice(1).join('\n\n');
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const optimizeRenderedText = (text: string): string => {
|
|
542
|
+
return text
|
|
543
|
+
.replace(/^\*Phase:\s+`[^`]+`\*\s*\n*/gm, '')
|
|
544
|
+
.replace(/^\s*<image\b[^>]*>\s*$/gim, '')
|
|
545
|
+
.replace(/^\s*<\/image>\s*$/gim, '')
|
|
546
|
+
.replace(/^\s*\[Image attached\]\s*$/gim, '')
|
|
547
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
548
|
+
.replace(/^```[^\n]*\n?/gm, '')
|
|
549
|
+
.replace(/\n```$/gm, '')
|
|
550
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
551
|
+
.replace(/^##\s+User\s*$/gm, 'User:')
|
|
552
|
+
.replace(/^##\s+Assistant\s*$/gm, 'Assistant:')
|
|
553
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
554
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
555
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
556
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
557
|
+
.trim();
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const optimizePlainText = (text: string): string => {
|
|
561
|
+
const normalized = text
|
|
562
|
+
.replace(/\r/g, '')
|
|
563
|
+
.replace(/^\s*<image\b[^>]*>\s*$/gim, '')
|
|
564
|
+
.replace(/^\s*<\/image>\s*$/gim, '')
|
|
565
|
+
.replace(/^\s*\[Image attached\]\s*$/gim, '')
|
|
566
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
567
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
568
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
569
|
+
.replace(/\*([^*\n]+)\*/g, '$1');
|
|
570
|
+
|
|
571
|
+
return normalized
|
|
572
|
+
.split('\n')
|
|
573
|
+
.map((line) => line.replace(/[ \t]+$/g, ''))
|
|
574
|
+
.join('\n')
|
|
575
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
576
|
+
.trim();
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const shouldSkipMessage = (role: string, text: string): boolean => {
|
|
580
|
+
if (text.startsWith('<environment_context>')) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (text.startsWith('# AGENTS.md instructions for ')) {
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (role === 'user' && text.includes('<environment_context>')) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return false;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const formatToolCallDetails = (tool: ToolRecord, outputFormat: ExportFormat): string => {
|
|
596
|
+
if (tool.name !== 'exec_command') {
|
|
597
|
+
return '';
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const details = parseExecCommandArguments(tool.argumentsText);
|
|
601
|
+
return details.cmd ? `Command: ${formatInlineLiteral(details.cmd, outputFormat)}` : '';
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const formatCompactToolCall = (tool: ToolRecord, outputFormat: ExportFormat): string => {
|
|
605
|
+
if (tool.name === 'exec_command') {
|
|
606
|
+
const details = parseExecCommandArguments(tool.argumentsText);
|
|
607
|
+
if (!details.cmd) {
|
|
608
|
+
return 'exec_command';
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const command = formatInlineLiteral(details.cmd, outputFormat);
|
|
612
|
+
return details.workdir ? `exec_command ${command} @ ${details.workdir}` : `exec_command ${command}`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return tool.callId ? `${tool.name} (${tool.callId})` : tool.name;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const formatCompactToolOutput = (outputText: string): string => {
|
|
619
|
+
if (!outputText) {
|
|
620
|
+
return '';
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const lines = outputText
|
|
624
|
+
.split('\n')
|
|
625
|
+
.map((line) => line.trim())
|
|
626
|
+
.filter(Boolean);
|
|
627
|
+
|
|
628
|
+
const exit = lines.find((line) => line.startsWith('Process exited with code '));
|
|
629
|
+
const wall = lines.find((line) => line.startsWith('Wall time: '));
|
|
630
|
+
|
|
631
|
+
if (exit && wall) {
|
|
632
|
+
return `${exit.replace('Process ', '')}; ${wall.toLowerCase()}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (exit) {
|
|
636
|
+
return exit.replace('Process ', '');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return '';
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const extractText = (content: JsonValue): string => {
|
|
643
|
+
if (typeof content === 'string') {
|
|
644
|
+
return content;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (Array.isArray(content)) {
|
|
648
|
+
const parts = content.map((item) => extractContentPart(item)).filter((part) => part.length > 0);
|
|
649
|
+
return parts.join('\n\n');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (content && typeof content === 'object') {
|
|
653
|
+
const text = asString((content as Record<string, JsonValue>).text);
|
|
654
|
+
if (text) {
|
|
655
|
+
return text;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return '';
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const extractContentPart = (value: JsonValue): string => {
|
|
663
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
664
|
+
return '';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const item = value as Record<string, JsonValue>;
|
|
668
|
+
const type = asString(item.type);
|
|
669
|
+
const text = asString(item.text);
|
|
670
|
+
|
|
671
|
+
if ((type === 'input_text' || type === 'output_text' || type === 'text') && text) {
|
|
672
|
+
return text;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (type === 'input_image') {
|
|
676
|
+
return '[Image attached]';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return text ?? '';
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const normalizeCompactLiteral = (value: string, outputFormat: ExportFormat): string => {
|
|
683
|
+
return outputFormat === 'md' ? value : value.replace(/`([^`]+)`/g, '$1');
|
|
684
|
+
};
|