@arcreflex/agent-transcripts 0.1.1 → 0.1.3
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 +36 -10
- package/src/parse.ts +63 -25
- package/src/render.ts +14 -6
- package/src/utils/naming.ts +173 -0
- 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,11 @@ 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
|
+
|
|
17
|
+
// Read OpenRouter API key from environment for LLM-based slug generation
|
|
18
|
+
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
|
|
16
19
|
|
|
17
20
|
// Shared options
|
|
18
21
|
const inputArg = positional({
|
|
@@ -25,7 +28,7 @@ const outputOpt = option({
|
|
|
25
28
|
type: optional(string),
|
|
26
29
|
long: "output",
|
|
27
30
|
short: "o",
|
|
28
|
-
description: "Output path (
|
|
31
|
+
description: "Output path (prints to stdout if not specified)",
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
const adapterOpt = option({
|
|
@@ -51,7 +54,19 @@ const parseCmd = command({
|
|
|
51
54
|
adapter: adapterOpt,
|
|
52
55
|
},
|
|
53
56
|
async handler({ input, output, adapter }) {
|
|
54
|
-
|
|
57
|
+
const naming = OPENROUTER_API_KEY
|
|
58
|
+
? { apiKey: OPENROUTER_API_KEY }
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
if (output) {
|
|
62
|
+
await parse({ input, output, adapter, naming });
|
|
63
|
+
} else {
|
|
64
|
+
// Print JSONL to stdout (one transcript per line)
|
|
65
|
+
const { transcripts } = await parseToTranscripts({ input, adapter });
|
|
66
|
+
for (const transcript of transcripts) {
|
|
67
|
+
console.log(JSON.stringify(transcript));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
55
70
|
},
|
|
56
71
|
});
|
|
57
72
|
|
|
@@ -80,13 +95,24 @@ const defaultCmd = command({
|
|
|
80
95
|
head: headOpt,
|
|
81
96
|
},
|
|
82
97
|
async handler({ input, output, adapter, head }) {
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
const naming = OPENROUTER_API_KEY
|
|
99
|
+
? { apiKey: OPENROUTER_API_KEY }
|
|
100
|
+
: undefined;
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
102
|
+
if (output) {
|
|
103
|
+
// Write intermediate JSON and markdown files
|
|
104
|
+
const { outputPaths } = await parse({ input, output, adapter, naming });
|
|
105
|
+
for (const jsonPath of outputPaths) {
|
|
106
|
+
const mdPath = jsonPath.replace(/\.json$/, ".md");
|
|
107
|
+
await render({ input: jsonPath, output: mdPath, head });
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// Stream to stdout - no intermediate files
|
|
111
|
+
const { transcripts } = await parseToTranscripts({ input, adapter });
|
|
112
|
+
for (let i = 0; i < transcripts.length; i++) {
|
|
113
|
+
if (i > 0) console.log(); // blank line between transcripts
|
|
114
|
+
console.log(renderTranscript(transcripts[i], head));
|
|
115
|
+
}
|
|
90
116
|
}
|
|
91
117
|
},
|
|
92
118
|
});
|
package/src/parse.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* Parse command: source format → intermediate JSON
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
6
|
import { mkdir } from "fs/promises";
|
|
7
7
|
import type { Transcript } from "./types.ts";
|
|
8
8
|
import { detectAdapter, getAdapter, listAdapters } from "./adapters/index.ts";
|
|
9
|
+
import { generateOutputName, type NamingOptions } from "./utils/naming.ts";
|
|
9
10
|
|
|
10
11
|
export interface ParseOptions {
|
|
11
12
|
input?: string; // file path, undefined for stdin
|
|
12
13
|
output?: string; // output path/dir
|
|
13
14
|
adapter?: string; // explicit adapter name
|
|
15
|
+
naming?: NamingOptions; // options for output file naming
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -40,27 +42,24 @@ async function readInput(
|
|
|
40
42
|
/**
|
|
41
43
|
* Determine output file paths for transcripts.
|
|
42
44
|
*/
|
|
43
|
-
function getOutputPaths(
|
|
45
|
+
async function getOutputPaths(
|
|
44
46
|
transcripts: Transcript[],
|
|
45
47
|
inputPath: string,
|
|
46
48
|
outputOption?: string,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let baseName: string;
|
|
50
|
-
if (inputPath === "<stdin>") {
|
|
51
|
-
baseName = "transcript";
|
|
52
|
-
} else {
|
|
53
|
-
const name = basename(inputPath);
|
|
54
|
-
baseName = name.replace(/\.jsonl?$/, "");
|
|
55
|
-
}
|
|
56
|
-
|
|
49
|
+
namingOptions?: NamingOptions,
|
|
50
|
+
): Promise<string[]> {
|
|
57
51
|
// Determine output directory
|
|
58
52
|
let outputDir: string;
|
|
53
|
+
let explicitBaseName: string | undefined;
|
|
54
|
+
|
|
59
55
|
if (outputOption) {
|
|
60
|
-
// If output looks like a file (has extension), use its directory
|
|
56
|
+
// If output looks like a file (has extension), use its directory and name
|
|
61
57
|
if (outputOption.match(/\.\w+$/)) {
|
|
62
58
|
outputDir = dirname(outputOption);
|
|
63
|
-
|
|
59
|
+
explicitBaseName = outputOption
|
|
60
|
+
.split("/")
|
|
61
|
+
.pop()!
|
|
62
|
+
.replace(/\.\w+$/, "");
|
|
64
63
|
} else {
|
|
65
64
|
outputDir = outputOption;
|
|
66
65
|
}
|
|
@@ -68,25 +67,50 @@ function getOutputPaths(
|
|
|
68
67
|
outputDir = process.cwd();
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
// Generate paths
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
// Generate paths with descriptive names
|
|
71
|
+
const paths: string[] = [];
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < transcripts.length; i++) {
|
|
74
|
+
let baseName: string;
|
|
75
|
+
|
|
76
|
+
if (explicitBaseName) {
|
|
77
|
+
// User provided explicit filename
|
|
78
|
+
baseName = explicitBaseName;
|
|
79
|
+
} else {
|
|
80
|
+
// Generate descriptive name
|
|
81
|
+
baseName = await generateOutputName(
|
|
82
|
+
transcripts[i],
|
|
83
|
+
inputPath,
|
|
84
|
+
namingOptions || {},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Add suffix for multiple transcripts
|
|
89
|
+
if (transcripts.length > 1) {
|
|
90
|
+
baseName = `${baseName}_${i + 1}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
paths.push(join(outputDir, `${baseName}.json`));
|
|
74
94
|
}
|
|
75
95
|
|
|
76
|
-
return
|
|
77
|
-
join(outputDir, `${baseName}_${i + 1}.json`),
|
|
78
|
-
);
|
|
96
|
+
return paths;
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
export interface ParseResult {
|
|
82
100
|
transcripts: Transcript[];
|
|
101
|
+
inputPath: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ParseAndWriteResult extends ParseResult {
|
|
83
105
|
outputPaths: string[];
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
87
|
-
* Parse source file(s) to
|
|
109
|
+
* Parse source file(s) to transcripts (no file I/O beyond reading input).
|
|
88
110
|
*/
|
|
89
|
-
export async function
|
|
111
|
+
export async function parseToTranscripts(
|
|
112
|
+
options: ParseOptions,
|
|
113
|
+
): Promise<ParseResult> {
|
|
90
114
|
const { content, path: inputPath } = await readInput(options.input);
|
|
91
115
|
|
|
92
116
|
// Determine adapter
|
|
@@ -108,11 +132,25 @@ export async function parse(options: ParseOptions): Promise<ParseResult> {
|
|
|
108
132
|
);
|
|
109
133
|
}
|
|
110
134
|
|
|
111
|
-
// Parse
|
|
112
135
|
const transcripts = adapter.parse(content, inputPath);
|
|
136
|
+
return { transcripts, inputPath };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse source file(s) to intermediate JSON and write to files.
|
|
141
|
+
*/
|
|
142
|
+
export async function parse(
|
|
143
|
+
options: ParseOptions,
|
|
144
|
+
): Promise<ParseAndWriteResult> {
|
|
145
|
+
const { transcripts, inputPath } = await parseToTranscripts(options);
|
|
113
146
|
|
|
114
147
|
// Write output files
|
|
115
|
-
const outputPaths = getOutputPaths(
|
|
148
|
+
const outputPaths = await getOutputPaths(
|
|
149
|
+
transcripts,
|
|
150
|
+
inputPath,
|
|
151
|
+
options.output,
|
|
152
|
+
options.naming,
|
|
153
|
+
);
|
|
116
154
|
|
|
117
155
|
for (let i = 0; i < transcripts.length; i++) {
|
|
118
156
|
const json = JSON.stringify(transcripts[i], null, 2);
|
|
@@ -123,5 +161,5 @@ export async function parse(options: ParseOptions): Promise<ParseResult> {
|
|
|
123
161
|
console.error(`Wrote: ${outputPaths[i]}`);
|
|
124
162
|
}
|
|
125
163
|
|
|
126
|
-
return { transcripts, outputPaths };
|
|
164
|
+
return { transcripts, inputPath, outputPaths };
|
|
127
165
|
}
|
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,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output file naming utilities.
|
|
3
|
+
*
|
|
4
|
+
* Generates descriptive filenames for transcripts:
|
|
5
|
+
* - With OpenRouter API key: yyyy-mm-dd-{llm-generated-slug}.{ext}
|
|
6
|
+
* - Without: yyyy-mm-dd-{input-filename-prefix}.{ext}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Transcript, UserMessage } from "../types.ts";
|
|
10
|
+
import { basename } from "path";
|
|
11
|
+
|
|
12
|
+
export interface NamingOptions {
|
|
13
|
+
apiKey?: string; // OpenRouter API key
|
|
14
|
+
model?: string; // Default: google/gemini-2.0-flash-001
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_MODEL = "google/gemini-2.0-flash-001";
|
|
18
|
+
const SLUG_MAX_LENGTH = 40;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract date from transcript's first message timestamp.
|
|
22
|
+
*/
|
|
23
|
+
function extractDate(transcript: Transcript): string {
|
|
24
|
+
const firstMessage = transcript.messages[0];
|
|
25
|
+
if (firstMessage?.timestamp) {
|
|
26
|
+
const date = new Date(firstMessage.timestamp);
|
|
27
|
+
if (!isNaN(date.getTime())) {
|
|
28
|
+
return date.toISOString().slice(0, 10); // yyyy-mm-dd
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Fallback to current date
|
|
32
|
+
return new Date().toISOString().slice(0, 10);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract context from transcript for LLM summarization.
|
|
37
|
+
* Uses first few user messages, truncated.
|
|
38
|
+
*/
|
|
39
|
+
function extractContext(transcript: Transcript): string {
|
|
40
|
+
const userMessages = transcript.messages.filter(
|
|
41
|
+
(m): m is UserMessage => m.type === "user",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const chunks: string[] = [];
|
|
45
|
+
let totalLength = 0;
|
|
46
|
+
const maxLength = 500;
|
|
47
|
+
|
|
48
|
+
for (const msg of userMessages.slice(0, 3)) {
|
|
49
|
+
const content = msg.content.slice(0, 200);
|
|
50
|
+
if (totalLength + content.length > maxLength) break;
|
|
51
|
+
chunks.push(content);
|
|
52
|
+
totalLength += content.length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return chunks.join("\n\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sanitize a string into a valid URL slug.
|
|
60
|
+
*/
|
|
61
|
+
function sanitizeSlug(input: string): string {
|
|
62
|
+
return input
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/[^a-z0-9\s-]/g, "") // remove special chars
|
|
65
|
+
.replace(/\s+/g, "-") // spaces to hyphens
|
|
66
|
+
.replace(/-+/g, "-") // collapse multiple hyphens
|
|
67
|
+
.replace(/^-|-$/g, "") // trim leading/trailing hyphens
|
|
68
|
+
.slice(0, SLUG_MAX_LENGTH);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate slug via OpenRouter API.
|
|
73
|
+
*/
|
|
74
|
+
async function generateSlugViaLLM(
|
|
75
|
+
context: string,
|
|
76
|
+
options: NamingOptions,
|
|
77
|
+
): Promise<string | null> {
|
|
78
|
+
const { apiKey, model = DEFAULT_MODEL } = options;
|
|
79
|
+
if (!apiKey || !context.trim()) return null;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(
|
|
83
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
84
|
+
{
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${apiKey}`,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
model,
|
|
92
|
+
messages: [
|
|
93
|
+
{
|
|
94
|
+
role: "user",
|
|
95
|
+
content: `Generate a 2-4 word URL slug (lowercase, hyphenated) summarizing this conversation topic. Reply with ONLY the slug, nothing else.\n\n${context}`,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
max_tokens: 20,
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
console.error(
|
|
105
|
+
`OpenRouter API error: ${response.status} ${response.statusText}`,
|
|
106
|
+
);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = (await response.json()) as {
|
|
111
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
112
|
+
};
|
|
113
|
+
const content = data.choices?.[0]?.message?.content?.trim();
|
|
114
|
+
|
|
115
|
+
if (!content) return null;
|
|
116
|
+
|
|
117
|
+
const slug = sanitizeSlug(content);
|
|
118
|
+
return slug || null;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(
|
|
121
|
+
`OpenRouter API call failed: ${error instanceof Error ? error.message : error}`,
|
|
122
|
+
);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate fallback slug from input filename.
|
|
129
|
+
*/
|
|
130
|
+
function generateFallbackSlug(inputPath: string): string {
|
|
131
|
+
return extractFileId(inputPath, 8) || "transcript";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract a short identifier from the input filename.
|
|
136
|
+
* Used as a suffix for traceability back to source.
|
|
137
|
+
*/
|
|
138
|
+
function extractFileId(inputPath: string, length = 6): string {
|
|
139
|
+
if (inputPath === "<stdin>") {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const name = basename(inputPath);
|
|
144
|
+
const base = name.replace(/\.jsonl?$/, "");
|
|
145
|
+
// Take first N chars, sanitize, and clean up any trailing hyphens
|
|
146
|
+
return sanitizeSlug(base.slice(0, length)).replace(/-+$/, "");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate output base name for a transcript.
|
|
151
|
+
* Returns string like "2024-01-15-implement-auth-flow-abc123"
|
|
152
|
+
*/
|
|
153
|
+
export async function generateOutputName(
|
|
154
|
+
transcript: Transcript,
|
|
155
|
+
inputPath: string,
|
|
156
|
+
options: NamingOptions = {},
|
|
157
|
+
): Promise<string> {
|
|
158
|
+
const date = extractDate(transcript);
|
|
159
|
+
const fileId = extractFileId(inputPath);
|
|
160
|
+
|
|
161
|
+
// Try LLM-generated slug if API key available
|
|
162
|
+
if (options.apiKey) {
|
|
163
|
+
const context = extractContext(transcript);
|
|
164
|
+
const slug = await generateSlugViaLLM(context, options);
|
|
165
|
+
if (slug) {
|
|
166
|
+
return fileId ? `${date}-${slug}-${fileId}` : `${date}-${slug}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fallback to input filename prefix (no need for fileId suffix, it's already the slug)
|
|
171
|
+
const slug = generateFallbackSlug(inputPath);
|
|
172
|
+
return `${date}-${slug}`;
|
|
173
|
+
}
|
|
@@ -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!
|