@arcreflex/agent-transcripts 0.1.1 → 0.1.2
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/.github/workflows/publish.yml +24 -0
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +55 -1
- package/src/cli.ts +26 -11
- package/src/parse.ts +19 -4
- package/src/render.ts +14 -6
- package/test/fixtures/claude/skipped-message-chain.input.jsonl +6 -0
- package/test/fixtures/claude/skipped-message-chain.output.md +28 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: oven-sh/setup-bun@v2
|
|
17
|
+
|
|
18
|
+
- run: bun install
|
|
19
|
+
|
|
20
|
+
- run: bun run check
|
|
21
|
+
|
|
22
|
+
- run: bun test
|
|
23
|
+
|
|
24
|
+
- run: npm publish --provenance --access public
|
package/package.json
CHANGED
|
@@ -199,6 +199,26 @@ function isToolResultOnly(content: string | ContentBlock[]): boolean {
|
|
|
199
199
|
return hasToolResult && !hasText;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Resolve a parent reference through any skipped messages.
|
|
204
|
+
* When messages are skipped (e.g., tool-result-only user messages),
|
|
205
|
+
* we redirect parent references to the skipped message's parent.
|
|
206
|
+
*/
|
|
207
|
+
function resolveParent(
|
|
208
|
+
parentUuid: string | null | undefined,
|
|
209
|
+
skippedParents: Map<string, string | undefined>,
|
|
210
|
+
): string | undefined {
|
|
211
|
+
if (!parentUuid) return undefined;
|
|
212
|
+
|
|
213
|
+
// Follow the chain through any skipped messages
|
|
214
|
+
let current: string | undefined = parentUuid;
|
|
215
|
+
while (current && skippedParents.has(current)) {
|
|
216
|
+
current = skippedParents.get(current);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return current;
|
|
220
|
+
}
|
|
221
|
+
|
|
202
222
|
/**
|
|
203
223
|
* Transform a conversation into our intermediate format.
|
|
204
224
|
*/
|
|
@@ -208,11 +228,45 @@ function transformConversation(
|
|
|
208
228
|
warnings: Warning[],
|
|
209
229
|
): Transcript {
|
|
210
230
|
const messages: Message[] = [];
|
|
231
|
+
// Track skipped message UUIDs → their parent UUIDs for chain repair
|
|
232
|
+
const skippedParents = new Map<string, string | undefined>();
|
|
233
|
+
|
|
234
|
+
// First pass: identify which messages will be skipped
|
|
235
|
+
for (const rec of records) {
|
|
236
|
+
if (!rec.uuid) continue;
|
|
237
|
+
|
|
238
|
+
let willSkip = false;
|
|
239
|
+
|
|
240
|
+
if (rec.type === "user" && rec.message) {
|
|
241
|
+
if (isToolResultOnly(rec.message.content)) {
|
|
242
|
+
willSkip = true;
|
|
243
|
+
} else {
|
|
244
|
+
const text = extractText(rec.message.content);
|
|
245
|
+
if (!text.trim()) willSkip = true;
|
|
246
|
+
}
|
|
247
|
+
} else if (rec.type === "assistant" && rec.message) {
|
|
248
|
+
const text = extractText(rec.message.content);
|
|
249
|
+
const thinking = extractThinking(rec.message.content);
|
|
250
|
+
const toolCalls = extractToolCalls(rec.message.content);
|
|
251
|
+
// Only skip if no text, no thinking, AND no tool calls
|
|
252
|
+
if (!text.trim() && !thinking && toolCalls.length === 0) {
|
|
253
|
+
willSkip = true;
|
|
254
|
+
}
|
|
255
|
+
} else if (rec.type === "system") {
|
|
256
|
+
const text = rec.content || "";
|
|
257
|
+
if (!text.trim()) willSkip = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (willSkip) {
|
|
261
|
+
skippedParents.set(rec.uuid, rec.parentUuid || undefined);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
211
264
|
|
|
265
|
+
// Second pass: build messages with corrected parent references
|
|
212
266
|
for (const rec of records) {
|
|
213
267
|
const sourceRef = rec.uuid || "";
|
|
214
268
|
const timestamp = rec.timestamp || new Date().toISOString();
|
|
215
|
-
const parentMessageRef = rec.parentUuid
|
|
269
|
+
const parentMessageRef = resolveParent(rec.parentUuid, skippedParents);
|
|
216
270
|
|
|
217
271
|
if (rec.type === "user" && rec.message) {
|
|
218
272
|
// Skip tool-result-only user messages (they're just tool responses)
|
package/src/cli.ts
CHANGED
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
optional,
|
|
12
12
|
positional,
|
|
13
13
|
} from "cmd-ts";
|
|
14
|
-
import { parse } from "./parse.ts";
|
|
15
|
-
import { render } from "./render.ts";
|
|
14
|
+
import { parse, parseToTranscripts } from "./parse.ts";
|
|
15
|
+
import { render, renderTranscript } from "./render.ts";
|
|
16
16
|
|
|
17
17
|
// Shared options
|
|
18
18
|
const inputArg = positional({
|
|
@@ -25,7 +25,7 @@ const outputOpt = option({
|
|
|
25
25
|
type: optional(string),
|
|
26
26
|
long: "output",
|
|
27
27
|
short: "o",
|
|
28
|
-
description: "Output path (
|
|
28
|
+
description: "Output path (prints to stdout if not specified)",
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
const adapterOpt = option({
|
|
@@ -51,7 +51,15 @@ const parseCmd = command({
|
|
|
51
51
|
adapter: adapterOpt,
|
|
52
52
|
},
|
|
53
53
|
async handler({ input, output, adapter }) {
|
|
54
|
-
|
|
54
|
+
if (output) {
|
|
55
|
+
await parse({ input, output, adapter });
|
|
56
|
+
} else {
|
|
57
|
+
// Print JSONL to stdout (one transcript per line)
|
|
58
|
+
const { transcripts } = await parseToTranscripts({ input, adapter });
|
|
59
|
+
for (const transcript of transcripts) {
|
|
60
|
+
console.log(JSON.stringify(transcript));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
55
63
|
},
|
|
56
64
|
});
|
|
57
65
|
|
|
@@ -80,13 +88,20 @@ const defaultCmd = command({
|
|
|
80
88
|
head: headOpt,
|
|
81
89
|
},
|
|
82
90
|
async handler({ input, output, adapter, head }) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
if (output) {
|
|
92
|
+
// Write intermediate JSON and markdown files
|
|
93
|
+
const { outputPaths } = await parse({ input, output, adapter });
|
|
94
|
+
for (const jsonPath of outputPaths) {
|
|
95
|
+
const mdPath = jsonPath.replace(/\.json$/, ".md");
|
|
96
|
+
await render({ input: jsonPath, output: mdPath, head });
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Stream to stdout - no intermediate files
|
|
100
|
+
const { transcripts } = await parseToTranscripts({ input, adapter });
|
|
101
|
+
for (let i = 0; i < transcripts.length; i++) {
|
|
102
|
+
if (i > 0) console.log(); // blank line between transcripts
|
|
103
|
+
console.log(renderTranscript(transcripts[i], head));
|
|
104
|
+
}
|
|
90
105
|
}
|
|
91
106
|
},
|
|
92
107
|
});
|
package/src/parse.ts
CHANGED
|
@@ -80,13 +80,19 @@ function getOutputPaths(
|
|
|
80
80
|
|
|
81
81
|
export interface ParseResult {
|
|
82
82
|
transcripts: Transcript[];
|
|
83
|
+
inputPath: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ParseAndWriteResult extends ParseResult {
|
|
83
87
|
outputPaths: string[];
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/**
|
|
87
|
-
* Parse source file(s) to
|
|
91
|
+
* Parse source file(s) to transcripts (no file I/O beyond reading input).
|
|
88
92
|
*/
|
|
89
|
-
export async function
|
|
93
|
+
export async function parseToTranscripts(
|
|
94
|
+
options: ParseOptions,
|
|
95
|
+
): Promise<ParseResult> {
|
|
90
96
|
const { content, path: inputPath } = await readInput(options.input);
|
|
91
97
|
|
|
92
98
|
// Determine adapter
|
|
@@ -108,8 +114,17 @@ export async function parse(options: ParseOptions): Promise<ParseResult> {
|
|
|
108
114
|
);
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
// Parse
|
|
112
117
|
const transcripts = adapter.parse(content, inputPath);
|
|
118
|
+
return { transcripts, inputPath };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse source file(s) to intermediate JSON and write to files.
|
|
123
|
+
*/
|
|
124
|
+
export async function parse(
|
|
125
|
+
options: ParseOptions,
|
|
126
|
+
): Promise<ParseAndWriteResult> {
|
|
127
|
+
const { transcripts, inputPath } = await parseToTranscripts(options);
|
|
113
128
|
|
|
114
129
|
// Write output files
|
|
115
130
|
const outputPaths = getOutputPaths(transcripts, inputPath, options.output);
|
|
@@ -123,5 +138,5 @@ export async function parse(options: ParseOptions): Promise<ParseResult> {
|
|
|
123
138
|
console.error(`Wrote: ${outputPaths[i]}`);
|
|
124
139
|
}
|
|
125
140
|
|
|
126
|
-
return { transcripts, outputPaths };
|
|
141
|
+
return { transcripts, inputPath, outputPaths };
|
|
127
142
|
}
|
package/src/render.ts
CHANGED
|
@@ -216,7 +216,10 @@ function tracePath(target: string, parents: Map<string, string>): string[] {
|
|
|
216
216
|
/**
|
|
217
217
|
* Render transcript to markdown with branch awareness.
|
|
218
218
|
*/
|
|
219
|
-
function renderTranscript(
|
|
219
|
+
export function renderTranscript(
|
|
220
|
+
transcript: Transcript,
|
|
221
|
+
head?: string,
|
|
222
|
+
): string {
|
|
220
223
|
const lines: string[] = [];
|
|
221
224
|
|
|
222
225
|
// Header
|
|
@@ -345,10 +348,15 @@ export async function render(options: RenderOptions): Promise<void> {
|
|
|
345
348
|
const { transcript, path: inputPath } = await readTranscript(options.input);
|
|
346
349
|
|
|
347
350
|
const markdown = renderTranscript(transcript, options.head);
|
|
348
|
-
const outputPath = getOutputPath(inputPath, options.output);
|
|
349
351
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
352
|
+
if (options.output) {
|
|
353
|
+
const outputPath = getOutputPath(inputPath, options.output);
|
|
354
|
+
// Ensure directory exists
|
|
355
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
356
|
+
await Bun.write(outputPath, markdown);
|
|
357
|
+
console.error(`Wrote: ${outputPath}`);
|
|
358
|
+
} else {
|
|
359
|
+
// Default: print to stdout
|
|
360
|
+
console.log(markdown);
|
|
361
|
+
}
|
|
354
362
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"type": "user", "uuid": "msg-1", "message": {"role": "user", "content": "Read the config file"}, "timestamp": "2024-01-15T10:00:00Z"}
|
|
2
|
+
{"type": "assistant", "uuid": "msg-2", "parentUuid": "msg-1", "message": {"role": "assistant", "content": [{"type": "text", "text": "I'll read the config file for you."}, {"type": "tool_use", "id": "tool-1", "name": "Read", "input": {"file_path": "/project/config.json"}}]}, "timestamp": "2024-01-15T10:00:02Z"}
|
|
3
|
+
{"type": "user", "uuid": "msg-3", "parentUuid": "msg-2", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tool-1", "content": "config contents here"}]}, "timestamp": "2024-01-15T10:00:03Z"}
|
|
4
|
+
{"type": "assistant", "uuid": "msg-4", "parentUuid": "msg-3", "message": {"role": "assistant", "content": [{"type": "text", "text": "The config file contains your settings."}]}, "timestamp": "2024-01-15T10:00:05Z"}
|
|
5
|
+
{"type": "user", "uuid": "msg-5", "parentUuid": "msg-4", "message": {"role": "user", "content": "Thanks!"}, "timestamp": "2024-01-15T10:00:10Z"}
|
|
6
|
+
{"type": "assistant", "uuid": "msg-6", "parentUuid": "msg-5", "message": {"role": "assistant", "content": [{"type": "text", "text": "You're welcome!"}]}, "timestamp": "2024-01-15T10:00:12Z"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Transcript
|
|
2
|
+
|
|
3
|
+
**Source**: `test/fixtures/claude/skipped-message-chain.input.jsonl`
|
|
4
|
+
**Adapter**: claude-code
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## User
|
|
9
|
+
|
|
10
|
+
Read the config file
|
|
11
|
+
|
|
12
|
+
## Assistant
|
|
13
|
+
|
|
14
|
+
I'll read the config file for you.
|
|
15
|
+
|
|
16
|
+
**Tool**: Read `/project/config.json`
|
|
17
|
+
|
|
18
|
+
## Assistant
|
|
19
|
+
|
|
20
|
+
The config file contains your settings.
|
|
21
|
+
|
|
22
|
+
## User
|
|
23
|
+
|
|
24
|
+
Thanks!
|
|
25
|
+
|
|
26
|
+
## Assistant
|
|
27
|
+
|
|
28
|
+
You're welcome!
|